Unverified Commit 92e21acb authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #1033 from open-webui/dev

0.1.109
parents eb51ad14 ad237b61
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
export let suggestionPrompts = []; export let suggestionPrompts = [];
export let autoScroll = true; export let autoScroll = true;
let chatTextAreaElement:HTMLTextAreaElement
let filesInputElement; let filesInputElement;
let promptsElement; let promptsElement;
...@@ -43,11 +43,9 @@ ...@@ -43,11 +43,9 @@
let speechRecognition; let speechRecognition;
$: if (prompt) { $: if (prompt) {
const chatInput = document.getElementById('chat-textarea'); if (chatTextAreaElement) {
chatTextAreaElement.style.height = '';
if (chatInput) { chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
chatInput.style.height = '';
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
} }
} }
...@@ -86,9 +84,7 @@ ...@@ -86,9 +84,7 @@
if (res) { if (res) {
prompt = res.text; prompt = res.text;
await tick(); await tick();
chatTextAreaElement?.focus();
const inputElement = document.getElementById('chat-textarea');
inputElement?.focus();
if (prompt !== '' && $settings?.speechAutoSend === true) { if (prompt !== '' && $settings?.speechAutoSend === true) {
submitPrompt(prompt, user); submitPrompt(prompt, user);
...@@ -191,8 +187,7 @@ ...@@ -191,8 +187,7 @@
prompt = `${prompt}${transcript}`; prompt = `${prompt}${transcript}`;
await tick(); await tick();
const inputElement = document.getElementById('chat-textarea'); chatTextAreaElement?.focus();
inputElement?.focus();
// Restart the inactivity timeout // Restart the inactivity timeout
timeoutId = setTimeout(() => { timeoutId = setTimeout(() => {
...@@ -294,8 +289,7 @@ ...@@ -294,8 +289,7 @@
}; };
onMount(() => { onMount(() => {
const chatInput = document.getElementById('chat-textarea'); window.setTimeout(() => chatTextAreaElement?.focus(), 0);
window.setTimeout(() => chatInput?.focus(), 0);
const dropZone = document.querySelector('body'); const dropZone = document.querySelector('body');
...@@ -663,6 +657,7 @@ ...@@ -663,6 +657,7 @@
<textarea <textarea
id="chat-textarea" id="chat-textarea"
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]"
......
...@@ -223,33 +223,80 @@ ...@@ -223,33 +223,80 @@
}, 100); }, 100);
}; };
// TODO: change delete behaviour const messageDeleteHandler = async (messageId) => {
// const deleteMessageAndDescendants = async (messageId: string) => { const messageToDelete = history.messages[messageId];
// if (history.messages[messageId]) { const messageParentId = messageToDelete.parentId;
// history.messages[messageId].deleted = true; const messageChildrenIds = messageToDelete.childrenIds ?? [];
const hasSibling = messageChildrenIds.some(
(childId) => history.messages[childId]?.childrenIds?.length > 0
);
messageChildrenIds.forEach((childId) => {
const child = history.messages[childId];
if (child && child.childrenIds) {
if (child.childrenIds.length === 0 && !hasSibling) {
// if last prompt/response pair
history.messages[messageParentId].childrenIds = [];
history.currentId = messageParentId;
} else {
child.childrenIds.forEach((grandChildId) => {
if (history.messages[grandChildId]) {
history.messages[grandChildId].parentId = messageParentId;
history.messages[messageParentId].childrenIds.push(grandChildId);
}
});
}
}
// remove response
history.messages[messageParentId].childrenIds = history.messages[
messageParentId
].childrenIds.filter((id) => id !== childId);
});
// remove prompt
history.messages[messageParentId].childrenIds = history.messages[
messageParentId
].childrenIds.filter((id) => id !== messageId);
await updateChatById(localStorage.token, chatId, {
messages: messages,
history: history
});
};
// const messageDeleteHandler = async (messageId) => {
// const message = history.messages[messageId];
// const parentId = message.parentId;
// const childrenIds = message.childrenIds ?? [];
// const grandchildrenIds = [];
// for (const childId of history.messages[messageId].childrenIds) { // // Iterate through childrenIds to find grandchildrenIds
// await deleteMessageAndDescendants(childId); // for (const childId of childrenIds) {
// const childMessage = history.messages[childId];
// const grandChildrenIds = childMessage.childrenIds ?? [];
// for (const grandchildId of grandchildrenIds) {
// const childMessage = history.messages[grandchildId];
// childMessage.parentId = parentId;
// } // }
// grandchildrenIds.push(...grandChildrenIds);
// } // }
// };
// const triggerDeleteMessageRecursive = async (messageId: string) => {
// await deleteMessageAndDescendants(messageId);
// await updateChatById(localStorage.token, chatId, { history });
// await chats.set(await getChatList(localStorage.token));
// };
const messageDeleteHandler = async (messageId) => { // history.messages[parentId].childrenIds.push(...grandchildrenIds);
if (history.messages[messageId]) { // history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
history.messages[messageId].deleted = true; // (id) => id !== messageId
// );
// // Select latest message
// let currentMessageId = grandchildrenIds.at(-1);
// if (currentMessageId) {
// let messageChildrenIds = history.messages[currentMessageId].childrenIds;
// while (messageChildrenIds.length !== 0) {
// currentMessageId = messageChildrenIds.at(-1);
// messageChildrenIds = history.messages[currentMessageId].childrenIds;
// }
// history.currentId = currentMessageId;
// }
for (const childId of history.messages[messageId].childrenIds) { // await updateChatById(localStorage.token, chatId, { messages, history });
history.messages[childId].deleted = true; // };
}
}
await updateChatById(localStorage.token, chatId, { history });
};
</script> </script>
{#if messages.length == 0} {#if messages.length == 0}
...@@ -258,57 +305,55 @@ ...@@ -258,57 +305,55 @@
<div class=" pb-10"> <div class=" pb-10">
{#key chatId} {#key chatId}
{#each messages as message, messageIdx} {#each messages as message, messageIdx}
{#if !message.deleted} <div class=" w-full">
<div class=" w-full"> <div
<div class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null
class="flex flex-col justify-between px-5 mb-3 {$settings?.fullScreenMode ?? null ? 'max-w-full'
? 'max-w-full' : 'max-w-3xl'} mx-auto rounded-lg group"
: 'max-w-3xl'} mx-auto rounded-lg group" >
> {#if message.role === 'user'}
{#if message.role === 'user'} <UserMessage
<UserMessage on:delete={() => messageDeleteHandler(message.id)}
on:delete={() => messageDeleteHandler(message.id)} user={$user}
user={$user} {message}
{message} isFirstMessage={messageIdx === 0}
isFirstMessage={messageIdx === 0} siblings={message.parentId !== null
siblings={message.parentId !== null ? history.messages[message.parentId]?.childrenIds ?? []
? history.messages[message.parentId]?.childrenIds ?? [] : Object.values(history.messages)
: Object.values(history.messages) .filter((message) => message.parentId === null)
.filter((message) => message.parentId === null) .map((message) => message.id) ?? []}
.map((message) => message.id) ?? []} {confirmEditMessage}
{confirmEditMessage} {showPreviousMessage}
{showPreviousMessage} {showNextMessage}
{showNextMessage} {copyToClipboard}
{copyToClipboard} />
/> {:else}
{:else} <ResponseMessage
<ResponseMessage {message}
{message} modelfiles={selectedModelfiles}
modelfiles={selectedModelfiles} siblings={history.messages[message.parentId]?.childrenIds ?? []}
siblings={history.messages[message.parentId]?.childrenIds ?? []} isLastMessage={messageIdx + 1 === messages.length}
isLastMessage={messageIdx + 1 === messages.length} {confirmEditResponseMessage}
{confirmEditResponseMessage} {showPreviousMessage}
{showPreviousMessage} {showNextMessage}
{showNextMessage} {rateMessage}
{rateMessage} {copyToClipboard}
{copyToClipboard} {continueGeneration}
{continueGeneration} {regenerateResponse}
{regenerateResponse} on:save={async (e) => {
on:save={async (e) => { console.log('save', e);
console.log('save', e);
const message = e.detail;
const message = e.detail; history.messages[message.id] = message;
history.messages[message.id] = message; await updateChatById(localStorage.token, chatId, {
await updateChatById(localStorage.token, chatId, { messages: messages,
messages: messages, history: history
history: history });
}); }}
}} />
/> {/if}
{/if}
</div>
</div> </div>
{/if} </div>
{/each} {/each}
{#if bottomPadding} {#if bottomPadding}
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
import CodeBlock from './CodeBlock.svelte'; import CodeBlock from './CodeBlock.svelte';
import Image from '$lib/components/common/Image.svelte'; import Image from '$lib/components/common/Image.svelte';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '$lib/components/common/Tooltip.svelte';
export let modelfiles = []; export let modelfiles = [];
export let message; export let message;
...@@ -40,7 +41,7 @@ ...@@ -40,7 +41,7 @@
let edit = false; let edit = false;
let editedContent = ''; let editedContent = '';
let editTextAreaElement: HTMLTextAreaElement;
let tooltipInstance = null; let tooltipInstance = null;
let sentencesAudio = {}; let sentencesAudio = {};
...@@ -247,10 +248,9 @@ ...@@ -247,10 +248,9 @@
editedContent = message.content; editedContent = message.content;
await tick(); await tick();
const editElement = document.getElementById(`message-edit-${message.id}`);
editElement.style.height = ''; editTextAreaElement.style.height = '';
editElement.style.height = `${editElement.scrollHeight}px`; editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
}; };
const editMessageConfirmHandler = async () => { const editMessageConfirmHandler = async () => {
...@@ -341,9 +341,11 @@ ...@@ -341,9 +341,11 @@
<div class=" w-full"> <div class=" w-full">
<textarea <textarea
id="message-edit-{message.id}" id="message-edit-{message.id}"
bind:this={editTextAreaElement}
class=" bg-transparent outline-none w-full resize-none" class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent} bind:value={editedContent}
on:input={(e) => { on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`; e.target.style.height = `${e.target.scrollHeight}px`;
}} }}
/> />
...@@ -462,145 +464,15 @@ ...@@ -462,145 +464,15 @@
</div> </div>
{/if} {/if}
<button <Tooltip content="Edit" placement="bottom">
class="{isLastMessage <button
? 'visible' class="{isLastMessage
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition" ? 'visible'
on:click={() => { : 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
editMessageHandler(); on:click={() => {
}} editMessageHandler();
> }}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
on:click={() => {
copyToClipboard(message.content);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/>
</svg>
</button>
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, 1);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
</button>
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, -1);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
> >
</button>
<button
id="speak-button-{message.id}"
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!loadingSpeech) {
toggleSpeakMessage(message);
}
}}
>
{#if loadingSpeech}
<svg
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
}
.spinner_Km9P {
animation-delay: -0.65s;
}
.spinner_JApP {
animation-delay: -0.5s;
}
@keyframes spinner_MGfb {
93.75%,
100% {
opacity: 0.2;
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
class="spinner_S1WN spinner_Km9P"
cx="12"
cy="12"
r="3"
/><circle class="spinner_S1WN spinner_JApP" cx="20" cy="12" r="3" /></svg
>
{:else if speaking}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
...@@ -612,10 +484,21 @@ ...@@ -612,10 +484,21 @@
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/> />
</svg> </svg>
{:else} </button>
</Tooltip>
<Tooltip content="Copy" placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition copy-response-button"
on:click={() => {
copyToClipboard(message.content);
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
...@@ -627,24 +510,79 @@ ...@@ -627,24 +510,79 @@
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/> />
</svg> </svg>
{/if} </button>
</button> </Tooltip>
{#if $config.images} <Tooltip content="Good Response" placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === 1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, 1);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
/></svg
>
</button>
</Tooltip>
<Tooltip content="Bad Response" placement="bottom">
<button <button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded {message.rating === -1
? 'bg-gray-100 dark:bg-gray-800'
: ''} dark:hover:text-white hover:text-black transition"
on:click={() => {
rateMessage(message.id, -1);
}}
>
<svg
stroke="currentColor"
fill="none"
stroke-width="2"
viewBox="0 0 24 24"
stroke-linecap="round"
stroke-linejoin="round"
class="w-4 h-4"
xmlns="http://www.w3.org/2000/svg"
><path
d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"
/></svg
>
</button>
</Tooltip>
<Tooltip content="Read Aloud" placement="bottom">
<button
id="speak-button-{message.id}"
class="{isLastMessage class="{isLastMessage
? 'visible' ? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition" : 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
if (!generatingImage) { if (!loadingSpeech) {
generateImage(message); toggleSpeakMessage(message);
} }
}} }}
> >
{#if generatingImage} {#if loadingSpeech}
<svg <svg
class=" w-4 h-4" class=" w-4 h-4"
fill="currentColor" fill="currentColor"
...@@ -679,6 +617,21 @@ ...@@ -679,6 +617,21 @@
r="3" r="3"
/></svg /></svg
> >
{:else if speaking}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.25 9.75 19.5 12m0 0 2.25 2.25M19.5 12l2.25-2.25M19.5 12l-2.25 2.25m-10.5-6 4.72-4.72a.75.75 0 0 1 1.28.53v15.88a.75.75 0 0 1-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.009 9.009 0 0 1 2.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75Z"
/>
</svg>
{:else} {:else}
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
...@@ -691,93 +644,166 @@ ...@@ -691,93 +644,166 @@
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" d="M19.114 5.636a9 9 0 010 12.728M16.463 8.288a5.25 5.25 0 010 7.424M6.75 8.25l4.72-4.72a.75.75 0 011.28.53v15.88a.75.75 0 01-1.28.53l-4.72-4.72H4.51c-.88 0-1.704-.507-1.938-1.354A9.01 9.01 0 012.25 12c0-.83.112-1.633.322-2.396C2.806 8.756 3.63 8.25 4.51 8.25H6.75z"
/> />
</svg> </svg>
{/if} {/if}
</button> </button>
</Tooltip>
{#if $config.images}
<Tooltip content="Generate Image" placement="bottom">
<button
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
if (!generatingImage) {
generateImage(message);
}
}}
>
{#if generatingImage}
<svg
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_S1WN {
animation: spinner_MGfb 0.8s linear infinite;
animation-delay: -0.8s;
}
.spinner_Km9P {
animation-delay: -0.65s;
}
.spinner_JApP {
animation-delay: -0.5s;
}
@keyframes spinner_MGfb {
93.75%,
100% {
opacity: 0.2;
}
}
</style><circle class="spinner_S1WN" cx="4" cy="12" r="3" /><circle
class="spinner_S1WN spinner_Km9P"
cx="12"
cy="12"
r="3"
/><circle
class="spinner_S1WN spinner_JApP"
cx="20"
cy="12"
r="3"
/></svg
>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
/>
</svg>
{/if}
</button>
</Tooltip>
{/if} {/if}
{#if message.info} {#if message.info}
<button <Tooltip content="Generation Info" placement="bottom">
class=" {isLastMessage <button
? 'visible' class=" {isLastMessage
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap" ? 'visible'
on:click={() => { : 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
console.log(message); on:click={() => {
}} console.log(message);
id="info-{message.id}" }}
> id="info-{message.id}"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" fill="none"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" viewBox="0 0 24 24"
/> stroke-width="1.5"
</svg> stroke="currentColor"
</button> class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z"
/>
</svg>
</button>
</Tooltip>
{/if} {/if}
{#if isLastMessage} {#if isLastMessage}
<button <Tooltip content="Continue Response" placement="bottom">
type="button" <button
class="{isLastMessage type="button"
? 'visible' class="{isLastMessage
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button" ? 'visible'
on:click={() => { : 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
continueGeneration(); on:click={() => {
}} continueGeneration();
> }}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" fill="none"
d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" viewBox="0 0 24 24"
/> stroke-width="1.5"
<path stroke="currentColor"
stroke-linecap="round" class="w-4 h-4"
stroke-linejoin="round" >
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z" <path
/> stroke-linecap="round"
</svg> stroke-linejoin="round"
</button> d="M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.91 11.672a.375.375 0 0 1 0 .656l-5.603 3.113a.375.375 0 0 1-.557-.328V8.887c0-.286.307-.466.557-.327l5.603 3.112Z"
/>
</svg>
</button>
</Tooltip>
<button <Tooltip content="Regenerate" placement="bottom">
type="button" <button
class="{isLastMessage type="button"
? 'visible' class="{isLastMessage
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button" ? 'visible'
on:click={regenerateResponse} : 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
> on:click={regenerateResponse}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" fill="none"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" viewBox="0 0 24 24"
/> stroke-width="1.5"
</svg> stroke="currentColor"
</button> class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99"
/>
</svg>
</button>
</Tooltip>
{/if} {/if}
</div> </div>
{/if} {/if}
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import Name from './Name.svelte'; import Name from './Name.svelte';
import ProfileImage from './ProfileImage.svelte'; import ProfileImage from './ProfileImage.svelte';
import { modelfiles, settings } from '$lib/stores'; import { modelfiles, settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -20,18 +21,17 @@ ...@@ -20,18 +21,17 @@
let edit = false; let edit = false;
let editedContent = ''; let editedContent = '';
let messageEditTextAreaElement: HTMLTextAreaElement;
const editMessageHandler = async () => { const editMessageHandler = async () => {
edit = true; edit = true;
editedContent = message.content; editedContent = message.content;
await tick(); await tick();
const editElement = document.getElementById(`message-edit-${message.id}`);
editElement.style.height = ''; messageEditTextAreaElement.style.height = '';
editElement.style.height = `${editElement.scrollHeight}px`; messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
editElement?.focus(); messageEditTextAreaElement?.focus();
}; };
const editMessageConfirmHandler = async () => { const editMessageConfirmHandler = async () => {
...@@ -165,9 +165,11 @@ ...@@ -165,9 +165,11 @@
<div class=" w-full"> <div class=" w-full">
<textarea <textarea
id="message-edit-{message.id}" id="message-edit-{message.id}"
bind:this={messageEditTextAreaElement}
class=" bg-transparent outline-none w-full resize-none" class=" bg-transparent outline-none w-full resize-none"
bind:value={editedContent} bind:value={editedContent}
on:input={(e) => { on:input={(e) => {
e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`; e.target.style.height = `${e.target.scrollHeight}px`;
}} }}
/> />
...@@ -245,55 +247,35 @@ ...@@ -245,55 +247,35 @@
</div> </div>
{/if} {/if}
<button <Tooltip content="Edit" placement="bottom">
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button" <button
on:click={() => { class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
editMessageHandler(); on:click={() => {
}} editMessageHandler();
> }}
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
copyToClipboard(message.content);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" fill="none"
d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" viewBox="0 0 24 24"
/> stroke-width="1.5"
</svg> stroke="currentColor"
</button> class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
</Tooltip>
{#if !isFirstMessage} <Tooltip content="Copy" placement="bottom">
<button <button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition" class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => { on:click={() => {
deleteMessageHandler(); copyToClipboard(message.content);
}} }}
> >
<svg <svg
...@@ -307,10 +289,36 @@ ...@@ -307,10 +289,36 @@
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"
/> />
</svg> </svg>
</button> </button>
</Tooltip>
{#if !isFirstMessage}
<Tooltip content="Delete" placement="bottom">
<button
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
on:click={() => {
deleteMessageHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
{/if} {/if}
</div> </div>
</div> </div>
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
let name = ''; let name = '';
let showJWTToken = false; let showJWTToken = false;
let JWTTokenCopied = false; let JWTTokenCopied = false;
let profileImageInputElement: HTMLInputElement;
const submitHandler = async () => { const submitHandler = async () => {
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch( const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
...@@ -40,11 +41,12 @@ ...@@ -40,11 +41,12 @@
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80"> <div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
<input <input
id="profile-image-input" id="profile-image-input"
bind:this={profileImageInputElement}
type="file" type="file"
hidden hidden
accept="image/*" accept="image/*"
on:change={(e) => { on:change={(e) => {
const files = e?.target?.files ?? []; const files = profileImageInputElement.files ?? [];
let reader = new FileReader(); let reader = new FileReader();
reader.onload = (event) => { reader.onload = (event) => {
let originalImageUrl = `${event.target.result}`; let originalImageUrl = `${event.target.result}`;
...@@ -86,7 +88,7 @@ ...@@ -86,7 +88,7 @@
// Display the compressed image // Display the compressed image
profileImageUrl = compressedSrc; profileImageUrl = compressedSrc;
e.target.files = null; profileImageInputElement.files = null;
}; };
}; };
...@@ -107,9 +109,7 @@ ...@@ -107,9 +109,7 @@
<button <button
class="relative rounded-full dark:bg-gray-700" class="relative rounded-full dark:bg-gray-700"
type="button" type="button"
on:click={() => { on:click={profileImageInputElement.click}
document.getElementById('profile-image-input')?.click();
}}
> >
<img <img
src={profileImageUrl !== '' ? profileImageUrl : '/user.png'} src={profileImageUrl !== '' ? profileImageUrl : '/user.png'}
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
let saveChatHistory = true; let saveChatHistory = true;
let importFiles; let importFiles;
let showDeleteConfirm = false; let showDeleteConfirm = false;
let chatImportInputElement: HTMLInputElement;
$: if (importFiles) { $: if (importFiles) {
console.log(importFiles); console.log(importFiles);
...@@ -159,12 +160,17 @@ ...@@ -159,12 +160,17 @@
<hr class=" dark:border-gray-700" /> <hr class=" dark:border-gray-700" />
<div class="flex flex-col"> <div class="flex flex-col">
<input id="chat-import-input" bind:files={importFiles} type="file" accept=".json" hidden /> <input
id="chat-import-input"
bind:this={chatImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
/>
<button <button
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition" class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={chatImportInputElement.click}
document.getElementById('chat-import-input').click();
}}
> >
<div class=" self-center mr-3"> <div class=" self-center mr-3">
<svg <svg
......
...@@ -3,14 +3,15 @@ ...@@ -3,14 +3,15 @@
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { getOllamaAPIUrl, getOllamaVersion, updateOllamaAPIUrl } from '$lib/apis/ollama'; import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai'; import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
export let getModels: Function; export let getModels: Function;
// External // External
let API_BASE_URL = ''; let OLLAMA_BASE_URL = '';
let OLLAMA_BASE_URLS = [''];
let OPENAI_API_KEY = ''; let OPENAI_API_KEY = '';
let OPENAI_API_BASE_URL = ''; let OPENAI_API_BASE_URL = '';
...@@ -25,8 +26,8 @@ ...@@ -25,8 +26,8 @@
await models.set(await getModels()); await models.set(await getModels());
}; };
const updateOllamaAPIUrlHandler = async () => { const updateOllamaUrlsHandler = async () => {
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL); OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => { const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
toast.error(error); toast.error(error);
...@@ -41,7 +42,7 @@ ...@@ -41,7 +42,7 @@
onMount(async () => { onMount(async () => {
if ($user.role === 'admin') { if ($user.role === 'admin') {
API_BASE_URL = await getOllamaAPIUrl(localStorage.token); OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token); OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
OPENAI_API_KEY = await getOpenAIKey(localStorage.token); OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
} }
...@@ -53,11 +54,6 @@ ...@@ -53,11 +54,6 @@
on:submit|preventDefault={() => { on:submit|preventDefault={() => {
updateOpenAIHandler(); updateOpenAIHandler();
dispatch('save'); dispatch('save');
// saveSettings({
// OPENAI_API_KEY: OPENAI_API_KEY !== '' ? OPENAI_API_KEY : undefined,
// OPENAI_API_BASE_URL: OPENAI_API_BASE_URL !== '' ? OPENAI_API_BASE_URL : undefined
// });
}} }}
> >
<div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3"> <div class=" pr-1.5 overflow-y-scroll max-h-[20.5rem] space-y-3">
...@@ -115,34 +111,81 @@ ...@@ -115,34 +111,81 @@
<div> <div>
<div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div> <div class=" mb-2.5 text-sm font-medium">Ollama Base URL</div>
<div class="flex w-full"> <div class="flex w-full gap-1.5">
<div class="flex-1 mr-2"> <div class="flex-1 flex flex-col gap-2">
<input {#each OLLAMA_BASE_URLS as url, idx}
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" <div class="flex gap-1.5">
placeholder="Enter URL (e.g. http://localhost:11434)" <input
bind:value={API_BASE_URL} class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
/> placeholder="Enter URL (e.g. http://localhost:11434)"
bind:value={url}
/>
<div class="self-center flex items-center">
{#if idx === 0}
<button
class="px-1"
on:click={() => {
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<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>
{:else}
<button
class="px-1"
on:click={() => {
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
}}
type="button"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
</svg>
</button>
{/if}
</div>
</div>
{/each}
</div> </div>
<button
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition" <div class="">
on:click={() => { <button
updateOllamaAPIUrlHandler(); class="p-2.5 bg-gray-200 hover:bg-gray-300 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-lg transition"
}} on:click={() => {
type="button" updateOllamaUrlsHandler();
> }}
<svg type="button"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
> >
<path <svg
fill-rule="evenodd" xmlns="http://www.w3.org/2000/svg"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z" viewBox="0 0 20 20"
clip-rule="evenodd" fill="currentColor"
/> class="w-4 h-4"
</svg> >
</button> <path
fill-rule="evenodd"
d="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
</div> </div>
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
......
...@@ -2,7 +2,13 @@ ...@@ -2,7 +2,13 @@
import queue from 'async/queue'; import queue from 'async/queue';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { createModel, deleteModel, getOllamaVersion, pullModel } from '$lib/apis/ollama'; import {
createModel,
deleteModel,
getOllamaUrls,
getOllamaVersion,
pullModel
} from '$lib/apis/ollama';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import { WEBUI_NAME, models, user } from '$lib/stores'; import { WEBUI_NAME, models, user } from '$lib/stores';
import { splitStream } from '$lib/utils'; import { splitStream } from '$lib/utils';
...@@ -13,7 +19,7 @@ ...@@ -13,7 +19,7 @@
let showLiteLLM = false; let showLiteLLM = false;
let showLiteLLMParams = false; let showLiteLLMParams = false;
let modelUploadInputElement: HTMLInputElement;
let liteLLMModelInfo = []; let liteLLMModelInfo = [];
let liteLLMModel = ''; let liteLLMModel = '';
...@@ -27,6 +33,9 @@ ...@@ -27,6 +33,9 @@
$: liteLLMModelName = liteLLMModel; $: liteLLMModelName = liteLLMModel;
// Models // Models
let OLLAMA_URLS = [];
let selectedOllamaUrlIdx: string | null = null;
let showExperimentalOllama = false; let showExperimentalOllama = false;
let ollamaVersion = ''; let ollamaVersion = '';
const MAX_PARALLEL_DOWNLOADS = 3; const MAX_PARALLEL_DOWNLOADS = 3;
...@@ -45,7 +54,7 @@ ...@@ -45,7 +54,7 @@
let modelUploadMode = 'file'; let modelUploadMode = 'file';
let modelInputFile = ''; let modelInputFile = '';
let modelFileUrl = ''; let modelFileUrl = '';
let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSSISTANT:"`; let modelFileContent = `TEMPLATE """{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: """\nPARAMETER num_ctx 4096\nPARAMETER stop "</s>"\nPARAMETER stop "USER:"\nPARAMETER stop "ASSISTANT:"`;
let modelFileDigest = ''; let modelFileDigest = '';
let uploadProgress = null; let uploadProgress = null;
...@@ -236,9 +245,11 @@ ...@@ -236,9 +245,11 @@
}; };
const deleteModelHandler = async () => { const deleteModelHandler = async () => {
const res = await deleteModel(localStorage.token, deleteModelTag).catch((error) => { const res = await deleteModel(localStorage.token, deleteModelTag, selectedOllamaUrlIdx).catch(
toast.error(error); (error) => {
}); toast.error(error);
}
);
if (res) { if (res) {
toast.success(`Deleted ${deleteModelTag}`); toast.success(`Deleted ${deleteModelTag}`);
...@@ -249,10 +260,12 @@ ...@@ -249,10 +260,12 @@
}; };
const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => { const pullModelHandlerProcessor = async (opts: { modelName: string; callback: Function }) => {
const res = await pullModel(localStorage.token, opts.modelName).catch((error) => { const res = await pullModel(localStorage.token, opts.modelName, selectedOllamaUrlIdx).catch(
opts.callback({ success: false, error, modelName: opts.modelName }); (error) => {
return null; opts.callback({ success: false, error, modelName: opts.modelName });
}); return null;
}
);
if (res) { if (res) {
const reader = res.body const reader = res.body
...@@ -358,6 +371,15 @@ ...@@ -358,6 +371,15 @@
}; };
onMount(async () => { onMount(async () => {
OLLAMA_URLS = await getOllamaUrls(localStorage.token).catch((error) => {
toast.error(error);
return [];
});
if (OLLAMA_URLS.length > 1) {
selectedOllamaUrlIdx = 0;
}
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false); ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => false);
liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token); liteLLMModelInfo = await getLiteLLMModelInfo(localStorage.token);
}); });
...@@ -367,52 +389,137 @@ ...@@ -367,52 +389,137 @@
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]"> <div class=" space-y-3 pr-1.5 overflow-y-scroll h-[23rem]">
{#if ollamaVersion} {#if ollamaVersion}
<div class="space-y-2 pr-1.5"> <div class="space-y-2 pr-1.5">
<div> <div class="text-sm font-medium">Manage Ollama Models</div>
<div class=" mb-2 text-sm font-medium">Manage Ollama Models</div>
{#if OLLAMA_URLS.length > 1}
<div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div> <div class="flex-1 pb-1">
<div class="flex w-full"> <select
<div class="flex-1 mr-2"> class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
<input bind:value={selectedOllamaUrlIdx}
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" placeholder="Select an Ollama instance"
placeholder="Enter model tag (e.g. mistral:7b)"
bind:value={modelTag}
/>
</div>
<button
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
pullModelHandler();
}}
disabled={modelTransferring}
> >
{#if modelTransferring} {#each OLLAMA_URLS as url, idx}
<div class="self-center"> <option value={idx} class="bg-gray-100 dark:bg-gray-700">{url}</option>
{/each}
</select>
</div>
{/if}
<div class="space-y-2">
<div>
<div class=" mb-2 text-sm font-medium">Pull a model from Ollama.com</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter model tag (e.g. mistral:7b)"
bind:value={modelTag}
/>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
pullModelHandler();
}}
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg <svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
><style> viewBox="0 0 16 16"
.spinner_ajPY { fill="currentColor"
transform-origin: center; class="w-4 h-4"
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
> >
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://ollama.com/library"
target="_blank">click here.</a
>
</div>
{#if Object.keys(modelDownloadStatus).length > 0}
{#each Object.keys(modelDownloadStatus) as model}
<div class="flex flex-col">
<div class="font-medium mb-1">{model}</div>
<div class="">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
>
{modelDownloadStatus[model].pullProgress ?? 0}%
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelDownloadStatus[model].digest}
</div>
</div>
</div> </div>
{:else} {/each}
{/if}
</div>
<div>
<div class=" mb-2 text-sm font-medium">Delete a model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteModelTag}
placeholder="Select a model"
>
{#if !deleteModelTag}
<option value="" disabled selected>Select a model</option>
{/if}
{#each $models.filter((m) => m.size != null && (selectedOllamaUrlIdx === null ? true : (m?.urls ?? []).includes(selectedOllamaUrlIdx))) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div>
<button
class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => {
deleteModelHandler();
}}
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16" viewBox="0 0 16 16"
...@@ -420,262 +527,192 @@ ...@@ -420,262 +527,192 @@
class="w-4 h-4" class="w-4 h-4"
> >
<path <path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z" fill-rule="evenodd"
/> d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
<path clip-rule="evenodd"
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/> />
</svg> </svg>
{/if} </button>
</button>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
To access the available model names for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://ollama.com/library"
target="_blank">click here.</a
>
</div>
{#if Object.keys(modelDownloadStatus).length > 0}
{#each Object.keys(modelDownloadStatus) as model}
<div class="flex flex-col">
<div class="font-medium mb-1">{model}</div>
<div class="">
<div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, modelDownloadStatus[model].pullProgress ?? 0)}%"
>
{modelDownloadStatus[model].pullProgress ?? 0}%
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelDownloadStatus[model].digest}
</div>
</div>
</div>
{/each}
{/if}
</div>
<div>
<div class=" mb-2 text-sm font-medium">Delete a model</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteModelTag}
placeholder="Select a model"
>
{#if !deleteModelTag}
<option value="" disabled selected>Select a model</option>
{/if}
{#each $models.filter((m) => m.size != null) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div> </div>
<button
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
on:click={() => {
deleteModelHandler();
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M5 3.25V4H2.75a.75.75 0 0 0 0 1.5h.3l.815 8.15A1.5 1.5 0 0 0 5.357 15h5.285a1.5 1.5 0 0 0 1.493-1.35l.815-8.15h.3a.75.75 0 0 0 0-1.5H11v-.75A2.25 2.25 0 0 0 8.75 1h-1.5A2.25 2.25 0 0 0 5 3.25Zm2.25-.75a.75.75 0 0 0-.75.75V4h3v-.75a.75.75 0 0 0-.75-.75h-1.5ZM6.05 6a.75.75 0 0 1 .787.713l.275 5.5a.75.75 0 0 1-1.498.075l-.275-5.5A.75.75 0 0 1 6.05 6Zm3.9 0a.75.75 0 0 1 .712.787l-.275 5.5a.75.75 0 0 1-1.498-.075l.275-5.5a.75.75 0 0 1 .786-.711Z"
clip-rule="evenodd"
/>
</svg>
</button>
</div> </div>
</div>
<div>
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">Experimental</div>
<button
class=" text-xs font-medium text-gray-500"
type="button"
on:click={() => {
showExperimentalOllama = !showExperimentalOllama;
}}>{showExperimentalOllama ? 'Show' : 'Hide'}</button
>
</div>
</div>
{#if showExperimentalOllama}
<form
on:submit|preventDefault={() => {
uploadModelHandler();
}}
>
<div class=" mb-2 flex w-full justify-between">
<div class=" text-sm font-medium">Upload a GGUF model</div>
<div class="pt-1">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">Experimental</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class=" text-xs font-medium text-gray-500"
on:click={() => {
if (modelUploadMode === 'file') {
modelUploadMode = 'url';
} else {
modelUploadMode = 'file';
}
}}
type="button" type="button"
on:click={() => {
showExperimentalOllama = !showExperimentalOllama;
}}>{showExperimentalOllama ? 'Hide' : 'Show'}</button
> >
{#if modelUploadMode === 'file'}
<span class="ml-2 self-center">File Mode</span>
{:else}
<span class="ml-2 self-center">URL Mode</span>
{/if}
</button>
</div> </div>
</div>
<div class="flex w-full mb-1.5"> {#if showExperimentalOllama}
<div class="flex flex-col w-full"> <form
{#if modelUploadMode === 'file'} on:submit|preventDefault={() => {
<div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}"> uploadModelHandler();
<input }}
id="model-upload-input" >
type="file" <div class=" mb-2 flex w-full justify-between">
bind:files={modelInputFile} <div class=" text-sm font-medium">Upload a GGUF model</div>
on:change={() => {
console.log(modelInputFile);
}}
accept=".gguf"
required
hidden
/>
<button
type="button"
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
on:click={() => {
document.getElementById('model-upload-input').click();
}}
>
{#if modelInputFile && modelInputFile.length > 0}
{modelInputFile[0].name}
{:else}
Click here to select
{/if}
</button>
</div>
{:else}
<div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
<input
class="w-full rounded text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
''
? 'mr-2'
: ''}"
type="url"
required
bind:value={modelFileUrl}
placeholder="Type Hugging Face Resolve (Download) URL"
/>
</div>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<button <button
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition" class="p-1 px-3 text-xs flex rounded transition"
type="submit" on:click={() => {
disabled={modelTransferring} if (modelUploadMode === 'file') {
modelUploadMode = 'url';
} else {
modelUploadMode = 'file';
}
}}
type="button"
> >
{#if modelTransferring} {#if modelUploadMode === 'file'}
<div class="self-center"> <span class="ml-2 self-center">File Mode</span>
<svg {:else}
class=" w-4 h-4" <span class="ml-2 self-center">URL Mode</span>
viewBox="0 0 24 24" {/if}
fill="currentColor" </button>
xmlns="http://www.w3.org/2000/svg" </div>
><style>
.spinner_ajPY { <div class="flex w-full mb-1.5">
transform-origin: center; <div class="flex flex-col w-full">
animation: spinner_AtaB 0.75s infinite linear; {#if modelUploadMode === 'file'}
} <div class="flex-1 {modelInputFile && modelInputFile.length > 0 ? 'mr-2' : ''}">
@keyframes spinner_AtaB { <input
100% { id="model-upload-input"
transform: rotate(360deg); bind:this={modelUploadInputElement}
} type="file"
} bind:files={modelInputFile}
</style><path on:change={() => {
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" console.log(modelInputFile);
opacity=".25" }}
/><path accept=".gguf"
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z" required
class="spinner_ajPY" hidden
/></svg />
<button
type="button"
class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850"
on:click={modelUploadInputElement.click}
> >
{#if modelInputFile && modelInputFile.length > 0}
{modelInputFile[0].name}
{:else}
Click here to select
{/if}
</button>
</div> </div>
{:else} {:else}
<svg <div class="flex-1 {modelFileUrl !== '' ? 'mr-2' : ''}">
xmlns="http://www.w3.org/2000/svg" <input
viewBox="0 0 16 16" class="w-full rounded-lg text-left py-2 px-4 dark:text-gray-300 dark:bg-gray-850 outline-none {modelFileUrl !==
fill="currentColor" ''
class="w-4 h-4" ? 'mr-2'
> : ''}"
<path type="url"
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z" required
/> bind:value={modelFileUrl}
<path placeholder="Type Hugging Face Resolve (Download) URL"
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/> />
</svg> </div>
{/if} {/if}
</button> </div>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')} {#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<div> <button
class="px-3 text-gray-100 bg-emerald-600 hover:bg-emerald-700 disabled:bg-gray-700 disabled:cursor-not-allowed rounded transition"
type="submit"
disabled={modelTransferring}
>
{#if modelTransferring}
<div class="self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M7.25 10.25a.75.75 0 0 0 1.5 0V4.56l2.22 2.22a.75.75 0 1 0 1.06-1.06l-3.5-3.5a.75.75 0 0 0-1.06 0l-3.5 3.5a.75.75 0 0 0 1.06 1.06l2.22-2.22v5.69Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
{/if}
</button>
{/if}
</div>
{#if (modelUploadMode === 'file' && modelInputFile && modelInputFile.length > 0) || (modelUploadMode === 'url' && modelFileUrl !== '')}
<div> <div>
<div class=" my-2.5 text-sm font-medium">Modelfile Content</div> <div>
<textarea <div class=" my-2.5 text-sm font-medium">Modelfile Content</div>
bind:value={modelFileContent} <textarea
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none" bind:value={modelFileContent}
rows="6" class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
/> rows="6"
/>
</div>
</div> </div>
{/if}
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the GGUF models available for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://huggingface.co/models?search=gguf"
target="_blank">click here.</a
>
</div> </div>
{/if}
<div class=" mt-1 text-xs text-gray-400 dark:text-gray-500">
To access the GGUF models available for downloading, <a
class=" text-gray-500 dark:text-gray-300 font-medium underline"
href="https://huggingface.co/models?search=gguf"
target="_blank">click here.</a
>
</div>
{#if uploadProgress !== null} {#if uploadProgress !== null}
<div class="mt-2"> <div class="mt-2">
<div class=" mb-2 text-xs">Upload Progress</div> <div class=" mb-2 text-xs">Upload Progress</div>
<div class="w-full rounded-full dark:bg-gray-800"> <div class="w-full rounded-full dark:bg-gray-800">
<div <div
class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full" class="dark:bg-gray-600 bg-gray-500 text-xs font-medium text-gray-100 text-center p-0.5 leading-none rounded-full"
style="width: {Math.max(15, uploadProgress ?? 0)}%" style="width: {Math.max(15, uploadProgress ?? 0)}%"
> >
{uploadProgress ?? 0}% {uploadProgress ?? 0}%
</div>
</div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;">
{modelFileDigest}
</div> </div>
</div> </div>
<div class="mt-1 text-xs dark:text-gray-500" style="font-size: 0.5rem;"> {/if}
{modelFileDigest} </form>
</div> {/if}
</div> </div>
{/if}
</form>
{/if}
</div> </div>
<hr class=" dark:border-gray-700 my-2" /> <hr class=" dark:border-gray-700 my-2" />
{/if} {/if}
...@@ -693,7 +730,7 @@ ...@@ -693,7 +730,7 @@
type="button" type="button"
on:click={() => { on:click={() => {
showLiteLLMParams = !showLiteLLMParams; showLiteLLMParams = !showLiteLLMParams;
}}>{showLiteLLMParams ? 'Advanced' : 'Default'}</button }}>{showLiteLLMParams ? 'Hide Additional Params' : 'Show Additional Params'}</button
> >
</div> </div>
</div> </div>
...@@ -702,7 +739,7 @@ ...@@ -702,7 +739,7 @@
<div class="flex w-full mb-1.5"> <div class="flex w-full mb-1.5">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM Model (litellm_params.model)" placeholder="Enter LiteLLM Model (litellm_params.model)"
bind:value={liteLLMModel} bind:value={liteLLMModel}
autocomplete="off" autocomplete="off"
...@@ -710,7 +747,7 @@ ...@@ -710,7 +747,7 @@
</div> </div>
<button <button
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition" class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
addLiteLLMModelHandler(); addLiteLLMModelHandler();
}} }}
...@@ -734,7 +771,7 @@ ...@@ -734,7 +771,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter Model Name (model_name)" placeholder="Enter Model Name (model_name)"
bind:value={liteLLMModelName} bind:value={liteLLMModelName}
autocomplete="off" autocomplete="off"
...@@ -748,7 +785,7 @@ ...@@ -748,7 +785,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)" placeholder="Enter LiteLLM API Base URL (litellm_params.api_base)"
bind:value={liteLLMAPIBase} bind:value={liteLLMAPIBase}
autocomplete="off" autocomplete="off"
...@@ -762,7 +799,7 @@ ...@@ -762,7 +799,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API Key (litellm_params.api_key)" placeholder="Enter LiteLLM API Key (litellm_params.api_key)"
bind:value={liteLLMAPIKey} bind:value={liteLLMAPIKey}
autocomplete="off" autocomplete="off"
...@@ -776,7 +813,7 @@ ...@@ -776,7 +813,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<input <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder="Enter LiteLLM API RPM (litellm_params.rpm)" placeholder="Enter LiteLLM API RPM (litellm_params.rpm)"
bind:value={liteLLMRPM} bind:value={liteLLMRPM}
autocomplete="off" autocomplete="off"
...@@ -803,7 +840,7 @@ ...@@ -803,7 +840,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<select <select
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={deleteLiteLLMModelId} bind:value={deleteLiteLLMModelId}
placeholder="Select a model" placeholder="Select a model"
> >
...@@ -818,7 +855,7 @@ ...@@ -818,7 +855,7 @@
</select> </select>
</div> </div>
<button <button
class="px-3 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition" class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
on:click={() => { on:click={() => {
deleteLiteLLMModelHandler(); deleteLiteLLMModelHandler();
}} }}
......
...@@ -29,6 +29,6 @@ ...@@ -29,6 +29,6 @@
}); });
</script> </script>
<div bind:this={tooltipElement}> <div bind:this={tooltipElement} aria-label={content}>
<slot /> <slot />
</div> </div>
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
export let show = false; export let show = false;
export let selectedDoc; export let selectedDoc;
let uploadDocInputElement: HTMLInputElement;
let inputFiles; let inputFiles;
let tags = []; let tags = [];
...@@ -69,7 +69,7 @@ ...@@ -69,7 +69,7 @@
} }
inputFiles = null; inputFiles = null;
document.getElementById('upload-doc-input').value = ''; uploadDocInputElement.value = '';
} else { } else {
toast.error(`File not found.`); toast.error(`File not found.`);
} }
...@@ -126,14 +126,19 @@ ...@@ -126,14 +126,19 @@
}} }}
> >
<div class="mb-3 w-full"> <div class="mb-3 w-full">
<input id="upload-doc-input" hidden bind:files={inputFiles} type="file" multiple /> <input
id="upload-doc-input"
bind:this={uploadDocInputElement}
hidden
bind:files={inputFiles}
type="file"
multiple
/>
<button <button
class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl" class="w-full text-sm font-medium py-3 bg-gray-850 hover:bg-gray-800 text-center rounded-xl"
type="button" type="button"
on:click={() => { on:click={uploadDocInputElement.click}
document.getElementById('upload-doc-input')?.click();
}}
> >
{#if inputFiles} {#if inputFiles}
{inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected. {inputFiles.length > 0 ? `${inputFiles.length}` : ''} document(s) selected.
......
...@@ -2,12 +2,11 @@ ...@@ -2,12 +2,11 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
export let messages = []; export let messages = [];
let textAreaElement: HTMLTextAreaElement;
onMount(() => { onMount(() => {
messages.forEach((message, idx) => { messages.forEach((message, idx) => {
let textareaElement = document.getElementById(`${message.role}-${idx}-textarea`); textAreaElement.style.height = '';
textareaElement.style.height = ''; textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
textareaElement.style.height = textareaElement.scrollHeight + 'px';
}); });
}); });
</script> </script>
...@@ -27,16 +26,17 @@ ...@@ -27,16 +26,17 @@
<div class="flex-1"> <div class="flex-1">
<textarea <textarea
id="{message.role}-{idx}-textarea" id="{message.role}-{idx}-textarea"
bind:this={textAreaElement}
class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden" class="w-full bg-transparent outline-none rounded-lg p-2 text-sm resize-none overflow-hidden"
placeholder="Enter {message.role === 'user' ? 'a user' : 'an assistant'} message here" placeholder="Enter {message.role === 'user' ? 'a user' : 'an assistant'} message here"
rows="1" rows="1"
on:input={(e) => { on:input={(e) => {
e.target.style.height = ''; textAreaElement.style.height = '';
e.target.style.height = e.target.scrollHeight + 'px'; textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
}} }}
on:focus={(e) => { on:focus={(e) => {
e.target.style.height = ''; textAreaElement.style.height = '';
e.target.style.height = e.target.scrollHeight + 'px'; textAreaElement.style.height = textAreaElement.scrollHeight + 'px';
// e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px'; // e.target.style.height = Math.min(e.target.scrollHeight, 200) + 'px';
}} }}
......
...@@ -7,7 +7,7 @@ export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``; ...@@ -7,7 +7,7 @@ export const WEBUI_BASE_URL = dev ? `http://${location.hostname}:8080` : ``;
export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`; export const WEBUI_API_BASE_URL = `${WEBUI_BASE_URL}/api/v1`;
export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`; export const LITELLM_API_BASE_URL = `${WEBUI_BASE_URL}/litellm/api`;
export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama/api`; export const OLLAMA_API_BASE_URL = `${WEBUI_BASE_URL}/ollama`;
export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`; export const OPENAI_API_BASE_URL = `${WEBUI_BASE_URL}/openai/api`;
export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`; export const AUDIO_API_BASE_URL = `${WEBUI_BASE_URL}/audio/api/v1`;
export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`; export const IMAGES_API_BASE_URL = `${WEBUI_BASE_URL}/images/api/v1`;
...@@ -90,8 +90,3 @@ export const SUPPORTED_FILE_EXTENSIONS = [ ...@@ -90,8 +90,3 @@ export const SUPPORTED_FILE_EXTENSIONS = [
// This feature, akin to $env/static/private, exclusively incorporates environment variables // This feature, akin to $env/static/private, exclusively incorporates environment variables
// that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_). // that are prefixed with config.kit.env.publicPrefix (usually set to PUBLIC_).
// Consequently, these variables can be securely exposed to client-side code. // Consequently, these variables can be securely exposed to client-side code.
// Example of the .env configuration:
// OLLAMA_API_BASE_URL="http://localhost:11434/api"
// # Public
// PUBLIC_API_BASE_URL=$OLLAMA_API_BASE_URL
...@@ -34,10 +34,11 @@ ...@@ -34,10 +34,11 @@
import Sidebar from '$lib/components/layout/Sidebar.svelte'; import Sidebar from '$lib/components/layout/Sidebar.svelte';
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte'; import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
import ChangelogModal from '$lib/components/ChangelogModal.svelte'; import ChangelogModal from '$lib/components/ChangelogModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
let ollamaVersion = ''; let ollamaVersion = '';
let loaded = false; let loaded = false;
let showShortcutsButtonElement: HTMLButtonElement;
let DB = null; let DB = null;
let localDBChats = []; let localDBChats = [];
...@@ -184,7 +185,7 @@ ...@@ -184,7 +185,7 @@
if (isCtrlPressed && event.key === '/') { if (isCtrlPressed && event.key === '/') {
event.preventDefault(); event.preventDefault();
console.log('showShortcuts'); console.log('showShortcuts');
document.getElementById('show-shortcuts-button')?.click(); showShortcutsButtonElement.click();
} }
}); });
...@@ -201,15 +202,18 @@ ...@@ -201,15 +202,18 @@
{#if loaded} {#if loaded}
<div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10"> <div class=" hidden lg:flex fixed bottom-0 right-0 px-3 py-3 z-10">
<button <Tooltip content="help" placement="left">
id="show-shortcuts-button" <button
class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full" id="show-shortcuts-button"
on:click={() => { bind:this={showShortcutsButtonElement}
showShortcuts = !showShortcuts; class="text-gray-600 dark:text-gray-300 bg-gray-300/20 w-6 h-6 flex items-center justify-center text-xs rounded-full"
}} on:click={() => {
> showShortcuts = !showShortcuts;
? }}
</button> >
?
</button>
</Tooltip>
</div> </div>
<ShortcutsModal bind:show={showShortcuts} /> <ShortcutsModal bind:show={showShortcuts} />
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
let processing = ''; let processing = '';
let messagesContainerElement: HTMLDivElement;
let currentRequestId = null; let currentRequestId = null;
let selectedModels = ['']; let selectedModels = [''];
...@@ -140,8 +140,7 @@ ...@@ -140,8 +140,7 @@
}; };
const scrollToBottom = () => { const scrollToBottom = () => {
const element = document.getElementById('messages-container'); messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
element.scrollTop = element.scrollHeight;
}; };
////////////////////////// //////////////////////////
...@@ -340,7 +339,7 @@ ...@@ -340,7 +339,7 @@
content: $settings.system content: $settings.system
} }
: undefined, : undefined,
...messages.filter((message) => !message.deleted) ...messages
] ]
.filter((message) => message) .filter((message) => message)
.map((message, idx, arr) => ({ .map((message, idx, arr) => ({
...@@ -548,7 +547,7 @@ ...@@ -548,7 +547,7 @@
content: $settings.system content: $settings.system
} }
: undefined, : undefined,
...messages.filter((message) => !message.deleted) ...messages
] ]
.filter((message) => message) .filter((message) => message)
.map((message, idx, arr) => ({ .map((message, idx, arr) => ({
...@@ -821,8 +820,11 @@ ...@@ -821,8 +820,11 @@
<div <div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
id="messages-container" id="messages-container"
bind:this={messagesContainerElement}
on:scroll={(e) => { on:scroll={(e) => {
autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50; autoScroll =
messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
messagesContainerElement.clientHeight + 50;
}} }}
> >
<div <div
...@@ -830,10 +832,7 @@ ...@@ -830,10 +832,7 @@
? 'max-w-full' ? 'max-w-full'
: 'max-w-2xl md:px-0'} mx-auto w-full px-4" : 'max-w-2xl md:px-0'} mx-auto w-full px-4"
> >
<ModelSelector <ModelSelector bind:selectedModels />
bind:selectedModels
disabled={messages.length > 0 && !selectedModels.includes('')}
/>
</div> </div>
<div class=" h-full w-full flex flex-col py-8"> <div class=" h-full w-full flex flex-col py-8">
......
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
let stopResponseFlag = false; let stopResponseFlag = false;
let autoScroll = true; let autoScroll = true;
let processing = ''; let processing = '';
let messagesContainerElement: HTMLDivElement;
let currentRequestId = null; let currentRequestId = null;
// let chatId = $page.params.id; // let chatId = $page.params.id;
...@@ -160,8 +160,7 @@ ...@@ -160,8 +160,7 @@
}; };
const scrollToBottom = () => { const scrollToBottom = () => {
const element = document.getElementById('messages-container'); messagesContainerElement.scrollTop = messagesContainerElement.scrollHeight;
element.scrollTop = element.scrollHeight;
}; };
////////////////////////// //////////////////////////
...@@ -353,7 +352,7 @@ ...@@ -353,7 +352,7 @@
content: $settings.system content: $settings.system
} }
: undefined, : undefined,
...messages.filter((message) => !message.deleted) ...messages
] ]
.filter((message) => message) .filter((message) => message)
.map((message, idx, arr) => ({ .map((message, idx, arr) => ({
...@@ -561,7 +560,7 @@ ...@@ -561,7 +560,7 @@
content: $settings.system content: $settings.system
} }
: undefined, : undefined,
...messages.filter((message) => !message.deleted) ...messages
] ]
.filter((message) => message) .filter((message) => message)
.map((message, idx, arr) => ({ .map((message, idx, arr) => ({
...@@ -852,8 +851,11 @@ ...@@ -852,8 +851,11 @@
<div <div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
id="messages-container" id="messages-container"
bind:this={messagesContainerElement}
on:scroll={(e) => { on:scroll={(e) => {
autoScroll = e.target.scrollHeight - e.target.scrollTop <= e.target.clientHeight + 50; autoScroll =
messagesContainerElement.scrollHeight - messagesContainerElement.scrollTop <=
messagesContainerElement.clientHeight + 50;
}} }}
> >
<div <div
...@@ -861,10 +863,7 @@ ...@@ -861,10 +863,7 @@
? 'max-w-full' ? 'max-w-full'
: 'max-w-2xl md:px-0'} mx-auto w-full px-4" : 'max-w-2xl md:px-0'} mx-auto w-full px-4"
> >
<ModelSelector <ModelSelector bind:selectedModels />
bind:selectedModels
disabled={messages.length > 0 && !selectedModels.includes('')}
/>
</div> </div>
<div class=" h-full w-full flex flex-col py-8"> <div class=" h-full w-full flex flex-col py-8">
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
let inputFiles = ''; let inputFiles = '';
let query = ''; let query = '';
let documentsImportInputElement: HTMLInputElement;
let tags = []; let tags = [];
let showSettingsModal = false; let showSettingsModal = false;
...@@ -524,6 +524,7 @@ ...@@ -524,6 +524,7 @@
<div class="flex space-x-2"> <div class="flex space-x-2">
<input <input
id="documents-import-input" id="documents-import-input"
bind:this={documentsImportInputElement}
bind:files={importFiles} bind:files={importFiles}
type="file" type="file"
accept=".json" accept=".json"
...@@ -558,9 +559,7 @@ ...@@ -558,9 +559,7 @@
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => { on:click={documentsImportInputElement.click}
document.getElementById('documents-import-input')?.click();
}}
> >
<div class=" self-center mr-2 font-medium">Import Documents Mapping</div> <div class=" self-center mr-2 font-medium">Import Documents Mapping</div>
......
...@@ -16,11 +16,14 @@ ...@@ -16,11 +16,14 @@
let localModelfiles = []; let localModelfiles = [];
let importFiles; let importFiles;
let modelfilesImportInputElement: HTMLInputElement;
const deleteModelHandler = async (tagName) => { const deleteModelHandler = async (tagName) => {
let success = null; let success = null;
success = await deleteModel(localStorage.token, tagName); success = await deleteModel(localStorage.token, tagName).catch((err) => {
toast.error(err);
return null;
});
if (success) { if (success) {
toast.success(`Deleted ${tagName}`); toast.success(`Deleted ${tagName}`);
...@@ -235,6 +238,7 @@ ...@@ -235,6 +238,7 @@
<div class="flex space-x-1"> <div class="flex space-x-1">
<input <input
id="modelfiles-import-input" id="modelfiles-import-input"
bind:this={modelfilesImportInputElement}
bind:files={importFiles} bind:files={importFiles}
type="file" type="file"
accept=".json" accept=".json"
...@@ -262,9 +266,7 @@ ...@@ -262,9 +266,7 @@
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => { on:click={modelfilesImportInputElement.click}
document.getElementById('modelfiles-import-input')?.click();
}}
> >
<div class=" self-center mr-2 font-medium">Import Modelfiles</div> <div class=" self-center mr-2 font-medium">Import Modelfiles</div>
......
<script> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
...@@ -21,15 +21,17 @@ ...@@ -21,15 +21,17 @@
let mode = 'chat'; let mode = 'chat';
let loaded = false; let loaded = false;
let text = ''; let text = '';
let selectedModelId = ''; let selectedModelId = '';
let loading = false; let loading = false;
let currentRequestId; let currentRequestId = null;
let stopResponseFlag = false; let stopResponseFlag = false;
let messagesContainerElement: HTMLDivElement;
let textCompletionAreaElement: HTMLTextAreaElement;
let system = ''; let system = '';
let messages = [ let messages = [
{ {
...@@ -39,13 +41,7 @@ ...@@ -39,13 +41,7 @@
]; ];
const scrollToBottom = () => { const scrollToBottom = () => {
let element; const element = mode === 'chat' ? messagesContainerElement : textCompletionAreaElement;
if (mode === 'chat') {
element = document.getElementById('messages-container');
} else {
element = document.getElementById('text-completion-textarea');
}
if (element) { if (element) {
element.scrollTop = element?.scrollHeight; element.scrollTop = element?.scrollHeight;
...@@ -96,6 +92,10 @@ ...@@ -96,6 +92,10 @@
while (true) { while (true) {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done || stopResponseFlag) { if (done || stopResponseFlag) {
if (stopResponseFlag) {
await cancelChatCompletion(localStorage.token, currentRequestId);
}
currentRequestId = null; currentRequestId = null;
break; break;
} }
...@@ -112,7 +112,11 @@ ...@@ -112,7 +112,11 @@
let data = JSON.parse(line.replace(/^data: /, '')); let data = JSON.parse(line.replace(/^data: /, ''));
console.log(data); console.log(data);
text += data.choices[0].delta.content ?? ''; if ('request_id' in data) {
currentRequestId = data.request_id;
} else {
text += data.choices[0].delta.content ?? '';
}
} }
} }
} }
...@@ -150,16 +154,6 @@ ...@@ -150,16 +154,6 @@
: `${OLLAMA_API_BASE_URL}/v1` : `${OLLAMA_API_BASE_URL}/v1`
); );
// const [res, controller] = await generateChatCompletion(localStorage.token, {
// model: selectedModelId,
// messages: [
// {
// role: 'assistant',
// content: text
// }
// ]
// });
let responseMessage; let responseMessage;
if (messages.at(-1)?.role === 'assistant') { if (messages.at(-1)?.role === 'assistant') {
responseMessage = messages.at(-1); responseMessage = messages.at(-1);
...@@ -184,6 +178,11 @@ ...@@ -184,6 +178,11 @@
while (true) { while (true) {
const { value, done } = await reader.read(); const { value, done } = await reader.read();
if (done || stopResponseFlag) { if (done || stopResponseFlag) {
if (stopResponseFlag) {
await cancelChatCompletion(localStorage.token, currentRequestId);
}
currentRequestId = null;
break; break;
} }
...@@ -200,17 +199,21 @@ ...@@ -200,17 +199,21 @@
let data = JSON.parse(line.replace(/^data: /, '')); let data = JSON.parse(line.replace(/^data: /, ''));
console.log(data); console.log(data);
if (responseMessage.content == '' && data.choices[0].delta.content == '\n') { if ('request_id' in data) {
continue; currentRequestId = data.request_id;
} else { } else {
textareaElement.style.height = textareaElement.scrollHeight + 'px'; if (responseMessage.content == '' && data.choices[0].delta.content == '\n') {
continue;
} else {
textareaElement.style.height = textareaElement.scrollHeight + 'px';
responseMessage.content += data.choices[0].delta.content ?? ''; responseMessage.content += data.choices[0].delta.content ?? '';
messages = messages; messages = messages;
textareaElement.style.height = textareaElement.scrollHeight + 'px'; textareaElement.style.height = textareaElement.scrollHeight + 'px';
await tick(); await tick();
}
} }
} }
} }
...@@ -221,48 +224,6 @@ ...@@ -221,48 +224,6 @@
scrollToBottom(); scrollToBottom();
} }
// while (true) {
// const { value, done } = await reader.read();
// if (done || stopResponseFlag) {
// if (stopResponseFlag) {
// await cancelChatCompletion(localStorage.token, currentRequestId);
// }
// currentRequestId = null;
// break;
// }
// try {
// let lines = value.split('\n');
// for (const line of lines) {
// if (line !== '') {
// console.log(line);
// let data = JSON.parse(line);
// if ('detail' in data) {
// throw data;
// }
// if ('id' in data) {
// console.log(data);
// currentRequestId = data.id;
// } else {
// if (data.done == false) {
// text += data.message.content;
// } else {
// console.log('done');
// }
// }
// }
// }
// } catch (error) {
// console.log(error);
// }
// scrollToBottom();
// }
} }
}; };
...@@ -417,12 +378,14 @@ ...@@ -417,12 +378,14 @@
<div <div
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0" class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0"
id="messages-container" id="messages-container"
bind:this={messagesContainerElement}
> >
<div class=" h-full w-full flex flex-col"> <div class=" h-full w-full flex flex-col">
<div class="flex-1 p-1"> <div class="flex-1 p-1">
{#if mode === 'complete'} {#if mode === 'complete'}
<textarea <textarea
id="text-completion-textarea" id="text-completion-textarea"
bind:this={textCompletionAreaElement}
class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm" class="w-full h-full p-3 bg-transparent outline outline-1 outline-gray-200 dark:outline-gray-800 resize-none rounded-lg text-sm"
bind:value={text} bind:value={text}
placeholder="You're a helpful assistant." placeholder="You're a helpful assistant."
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
let importFiles = ''; let importFiles = '';
let query = ''; let query = '';
let promptsImportInputElement: HTMLInputElement;
const sharePrompt = async (prompt) => { const sharePrompt = async (prompt) => {
toast.success('Redirecting you to OpenWebUI Community'); toast.success('Redirecting you to OpenWebUI Community');
...@@ -208,6 +208,7 @@ ...@@ -208,6 +208,7 @@
<div class="flex space-x-2"> <div class="flex space-x-2">
<input <input
id="prompts-import-input" id="prompts-import-input"
bind:this={promptsImportInputElement}
bind:files={importFiles} bind:files={importFiles}
type="file" type="file"
accept=".json" accept=".json"
...@@ -241,9 +242,7 @@ ...@@ -241,9 +242,7 @@
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => { on:click={promptsImportInputElement.click}
document.getElementById('prompts-import-input')?.click();
}}
> >
<div class=" self-center mr-2 font-medium">Import Prompts</div> <div class=" self-center mr-2 font-medium">Import Prompts</div>
...@@ -266,7 +265,7 @@ ...@@ -266,7 +265,7 @@
<button <button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition" class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => { on:click={async () => {
// document.getElementById('modelfiles-import-input')?.click(); // promptsImportInputElement.click();
let blob = new Blob([JSON.stringify($prompts)], { let blob = new Blob([JSON.stringify($prompts)], {
type: 'application/json' type: 'application/json'
}); });
......
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