mirror of
https://github.com/open-webui/open-webui
synced 2025-06-26 18:26:48 +00:00
Merge branch 'main' into feat/cancel-model-download
# Conflicts: # src/lib/components/chat/Settings/Models.svelte
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
import { blobToFile, calculateSHA256, findWordIndices } from '$lib/utils';
|
||||
|
||||
@@ -12,13 +12,16 @@
|
||||
import Documents from './MessageInput/Documents.svelte';
|
||||
import Models from './MessageInput/Models.svelte';
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let submitPrompt: Function;
|
||||
export let stopResponse: Function;
|
||||
|
||||
export let suggestionPrompts = [];
|
||||
export let autoScroll = true;
|
||||
|
||||
let chatTextAreaElement: HTMLTextAreaElement;
|
||||
let filesInputElement;
|
||||
|
||||
let promptsElement;
|
||||
@@ -42,11 +45,9 @@
|
||||
let speechRecognition;
|
||||
|
||||
$: if (prompt) {
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
|
||||
if (chatInput) {
|
||||
chatInput.style.height = '';
|
||||
chatInput.style.height = Math.min(chatInput.scrollHeight, 200) + 'px';
|
||||
if (chatTextAreaElement) {
|
||||
chatTextAreaElement.style.height = '';
|
||||
chatTextAreaElement.style.height = Math.min(chatTextAreaElement.scrollHeight, 200) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,9 +86,7 @@
|
||||
if (res) {
|
||||
prompt = res.text;
|
||||
await tick();
|
||||
|
||||
const inputElement = document.getElementById('chat-textarea');
|
||||
inputElement?.focus();
|
||||
chatTextAreaElement?.focus();
|
||||
|
||||
if (prompt !== '' && $settings?.speechAutoSend === true) {
|
||||
submitPrompt(prompt, user);
|
||||
@@ -190,8 +189,7 @@
|
||||
prompt = `${prompt}${transcript}`;
|
||||
|
||||
await tick();
|
||||
const inputElement = document.getElementById('chat-textarea');
|
||||
inputElement?.focus();
|
||||
chatTextAreaElement?.focus();
|
||||
|
||||
// Restart the inactivity timeout
|
||||
timeoutId = setTimeout(() => {
|
||||
@@ -213,11 +211,11 @@
|
||||
// Event triggered when an error occurs
|
||||
speechRecognition.onerror = function (event) {
|
||||
console.log(event);
|
||||
toast.error(`Speech recognition error: ${event.error}`);
|
||||
toast.error($i18n.t(`Speech recognition error: {{error}}`, { error: event.error }));
|
||||
isRecording = false;
|
||||
};
|
||||
} else {
|
||||
toast.error('SpeechRecognition API is not supported in this browser.');
|
||||
toast.error($i18n.t('SpeechRecognition API is not supported in this browser.'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -293,6 +291,8 @@
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.setTimeout(() => chatTextAreaElement?.focus(), 0);
|
||||
|
||||
const dropZone = document.querySelector('body');
|
||||
|
||||
const onDragOver = (e) => {
|
||||
@@ -335,12 +335,15 @@
|
||||
uploadDoc(file);
|
||||
} else {
|
||||
toast.error(
|
||||
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
||||
$i18n.t(
|
||||
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
||||
{ file_type: file['type'] }
|
||||
)
|
||||
);
|
||||
uploadDoc(file);
|
||||
}
|
||||
} else {
|
||||
toast.error(`File not found.`);
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,12 +364,12 @@
|
||||
|
||||
{#if dragged}
|
||||
<div
|
||||
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
|
||||
class="fixed lg:w-[calc(100%-260px)] w-full h-full flex z-50 touch-none pointer-events-none"
|
||||
id="dropzone"
|
||||
role="region"
|
||||
aria-label="Drag and Drop Container"
|
||||
>
|
||||
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="absolute w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
|
||||
<div class="m-auto pt-64 flex flex-col justify-center">
|
||||
<div class="max-w-md">
|
||||
<AddFilesPlaceholder />
|
||||
@@ -479,18 +482,21 @@
|
||||
filesInputElement.value = '';
|
||||
} else {
|
||||
toast.error(
|
||||
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
|
||||
$i18n.t(
|
||||
`Unknown File Type '{{file_type}}', but accepting and treating as plain text`,
|
||||
{ file_type: file['type'] }
|
||||
)
|
||||
);
|
||||
uploadDoc(file);
|
||||
filesInputElement.value = '';
|
||||
}
|
||||
} else {
|
||||
toast.error(`File not found.`);
|
||||
toast.error($i18n.t(`File not found.`));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<form
|
||||
class=" flex flex-col relative w-full rounded-xl border dark:border-gray-700 bg-white dark:bg-gray-900 dark:text-gray-100"
|
||||
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={() => {
|
||||
submitPrompt(prompt, user);
|
||||
}}
|
||||
@@ -572,7 +578,7 @@
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">Document</div>
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if file.type === 'collection'}
|
||||
@@ -600,7 +606,7 @@
|
||||
{file?.title ?? `#${file.name}`}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">Collection</div>
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -633,40 +639,41 @@
|
||||
|
||||
<div class=" flex">
|
||||
{#if fileUploadEnabled}
|
||||
<div class=" self-end mb-2 ml-1.5">
|
||||
<button
|
||||
class=" text-gray-600 dark:text-gray-200 transition rounded-lg p-1 ml-1"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
filesInputElement.click();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
<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();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M15.621 4.379a3 3 0 00-4.242 0l-7 7a3 3 0 004.241 4.243h.001l.497-.5a.75.75 0 011.064 1.057l-.498.501-.002.002a4.5 4.5 0 01-6.364-6.364l7-7a4.5 4.5 0 016.368 6.36l-3.455 3.553A2.625 2.625 0 119.52 9.52l3.45-3.451a.75.75 0 111.061 1.06l-3.45 3.451a1.125 1.125 0 001.587 1.595l3.454-3.553a3 3 0 000-4.242z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<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
|
||||
id="chat-textarea"
|
||||
class=" dark:bg-gray-900 dark:text-gray-100 outline-none w-full py-3 px-2 {fileUploadEnabled
|
||||
bind:this={chatTextAreaElement}
|
||||
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]"
|
||||
placeholder={chatInputPlaceholder !== ''
|
||||
? chatInputPlaceholder
|
||||
: isRecording
|
||||
? 'Listening...'
|
||||
: 'Send a message'}
|
||||
? $i18n.t('Listening...')
|
||||
: $i18n.t('Send a Message')}
|
||||
bind:value={prompt}
|
||||
on:keypress={(e) => {
|
||||
if (e.keyCode == 13 && !e.shiftKey) {
|
||||
@@ -803,92 +810,102 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="self-end mb-2 flex space-x-0.5 mr-2">
|
||||
<div class="self-end mb-2 flex space-x-1 mr-1">
|
||||
{#if messages.length == 0 || messages.at(-1).done == true}
|
||||
{#if speechRecognitionEnabled}
|
||||
<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;
|
||||
}
|
||||
.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
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
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')}>
|
||||
<button
|
||||
id="voice-input-button"
|
||||
class=" text-gray-600 dark:text-gray-300 transition rounded-lg p-1.5 mr-0.5 self-center"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
speechRecognitionHandler();
|
||||
}}
|
||||
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 === ''}
|
||||
>
|
||||
{#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;
|
||||
}
|
||||
.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
|
||||
>
|
||||
{:else}
|
||||
<svg
|
||||
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}
|
||||
<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>
|
||||
{/if}
|
||||
<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-lg p-1 mr-0.5 w-7 h-7 self-center"
|
||||
type="submit"
|
||||
disabled={prompt === ''}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4.5 h-4.5 mx-auto"
|
||||
>
|
||||
<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
|
||||
class="bg-white hover:bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-white dark:hover:bg-gray-800 transition rounded-lg p-1.5"
|
||||
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
|
||||
@@ -910,7 +927,7 @@
|
||||
</form>
|
||||
|
||||
<div class="mt-1.5 text-xs text-gray-500 text-center">
|
||||
LLMs can make mistakes. Verify important information.
|
||||
{$i18n.t('LLMs can make mistakes. Verify important information.')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
import { documents } from '$lib/stores';
|
||||
import { removeFirstHashWord, isValidHttpUrl } from '$lib/utils';
|
||||
import { tick } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let prompt = '';
|
||||
|
||||
@@ -89,16 +91,16 @@
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">#</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
|
||||
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
{#each filteredItems as doc, docIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-lg w-full text-left {docIdx === selectedIdx
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {docIdx === selectedIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
@@ -117,7 +119,7 @@
|
||||
{doc?.title ?? `#${doc.name}`}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">Collection</div>
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Collection')}</div>
|
||||
{:else}
|
||||
<div class=" font-medium text-black line-clamp-1">
|
||||
#{doc.name} ({doc.filename})
|
||||
@@ -132,7 +134,7 @@
|
||||
|
||||
{#if prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
<button
|
||||
class="px-3 py-1.5 rounded-lg w-full text-left bg-gray-100 selected-command-option-button"
|
||||
class="px-3 py-1.5 rounded-xl w-full text-left bg-gray-100 selected-command-option-button"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
const url = prompt.split(' ')?.at(0)?.substring(1);
|
||||
@@ -140,7 +142,9 @@
|
||||
confirmSelectWeb(url);
|
||||
} else {
|
||||
toast.error(
|
||||
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
||||
$i18n.t(
|
||||
'Oops! Looks like the URL is invalid. Please double-check and try again.'
|
||||
)
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -149,7 +153,7 @@
|
||||
{prompt.split(' ')?.at(0)?.substring(1)}
|
||||
</div>
|
||||
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">Web</div>
|
||||
<div class=" text-xs text-gray-600 line-clamp-1">{$i18n.t('Web')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
import { generatePrompt } from '$lib/apis/ollama';
|
||||
import { models } from '$lib/stores';
|
||||
import { splitStream } from '$lib/utils';
|
||||
import { tick } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let prompt = '';
|
||||
export let user = null;
|
||||
@@ -41,7 +43,7 @@
|
||||
user = JSON.parse(JSON.stringify(model.name));
|
||||
await tick();
|
||||
|
||||
chatInputPlaceholder = `'${model.name}' is thinking...`;
|
||||
chatInputPlaceholder = $i18n.t('{{modelName}} is thinking...', { modelName: model.name });
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
|
||||
@@ -79,14 +81,18 @@
|
||||
throw data;
|
||||
}
|
||||
|
||||
if (data.done == false) {
|
||||
if (prompt == '' && data.response == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
prompt += data.response;
|
||||
console.log(data.response);
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
await tick();
|
||||
if ('id' in data) {
|
||||
console.log(data);
|
||||
} else {
|
||||
if (data.done == false) {
|
||||
if (prompt == '' && data.response == '\n') {
|
||||
continue;
|
||||
} else {
|
||||
prompt += data.response;
|
||||
console.log(data.response);
|
||||
chatInputElement.scrollTop = chatInputElement.scrollHeight;
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -109,7 +115,9 @@
|
||||
toast.error(error.error);
|
||||
}
|
||||
} else {
|
||||
toast.error(`Uh-oh! There was an issue connecting to Ollama.`);
|
||||
toast.error(
|
||||
$i18n.t('Uh-oh! There was an issue connecting to {{provider}}.', { provider: 'llama' })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,16 +129,16 @@
|
||||
|
||||
{#if filteredModels.length > 0}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">@</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
|
||||
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
{#each filteredModels as model, modelIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-lg w-full text-left {modelIdx === selectedIdx
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {modelIdx === selectedIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { prompts } from '$lib/stores';
|
||||
import { findWordIndices } from '$lib/utils';
|
||||
import { tick } from 'svelte';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let prompt = '';
|
||||
let selectedCommandIdx = 0;
|
||||
@@ -24,7 +27,18 @@
|
||||
};
|
||||
|
||||
const confirmCommand = async (command) => {
|
||||
prompt = command.content;
|
||||
let text = command.content;
|
||||
|
||||
if (command.content.includes('{{CLIPBOARD}}')) {
|
||||
const clipboardText = await navigator.clipboard.readText().catch((err) => {
|
||||
toast.error($i18n.t('Failed to read clipboard contents'));
|
||||
return '{{CLIPBOARD}}';
|
||||
});
|
||||
|
||||
text = command.content.replaceAll('{{CLIPBOARD}}', clipboardText);
|
||||
}
|
||||
|
||||
prompt = text;
|
||||
|
||||
const chatInputElement = document.getElementById('chat-textarea');
|
||||
|
||||
@@ -48,16 +62,16 @@
|
||||
|
||||
{#if filteredPromptCommands.length > 0}
|
||||
<div class="md:px-2 mb-3 text-left w-full absolute bottom-0 left-0 right-0">
|
||||
<div class="flex w-full rounded-lg border border-gray-100 dark:border-gray-700">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-lg text-center">
|
||||
<div class="flex w-full px-2">
|
||||
<div class=" bg-gray-100 dark:bg-gray-700 w-10 rounded-l-xl text-center">
|
||||
<div class=" text-lg font-semibold mt-2">/</div>
|
||||
</div>
|
||||
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-lg">
|
||||
<div class=" overflow-y-auto bg-white p-2 rounded-tr-lg space-y-0.5">
|
||||
<div class="max-h-60 flex flex-col w-full rounded-r-xl bg-white">
|
||||
<div class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5">
|
||||
{#each filteredPromptCommands as command, commandIdx}
|
||||
<button
|
||||
class=" px-3 py-1.5 rounded-lg w-full text-left {commandIdx === selectedCommandIdx
|
||||
class=" px-3 py-1.5 rounded-xl w-full text-left {commandIdx === selectedCommandIdx
|
||||
? ' bg-gray-100 selected-command-option-button'
|
||||
: ''}"
|
||||
type="button"
|
||||
@@ -81,7 +95,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-lg flex items-center space-x-1"
|
||||
class=" px-2 pb-1 text-xs text-gray-600 bg-white rounded-br-xl flex items-center space-x-1"
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
@@ -101,8 +115,9 @@
|
||||
</div>
|
||||
|
||||
<div class="line-clamp-1">
|
||||
Tip: Update multiple variable slots consecutively by pressing the tab key in the chat
|
||||
input after each replacement.
|
||||
{$i18n.t(
|
||||
'Tip: Update multiple variable slots consecutively by pressing the tab key in the chat input after each replacement.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,45 +10,47 @@
|
||||
: suggestionPrompts.sort(() => Math.random() - 0.5).slice(0, 4);
|
||||
</script>
|
||||
|
||||
<div class=" flex flex-wrap-reverse mb-3 md:p-1 text-left w-full">
|
||||
{#each prompts as prompt, promptIdx}
|
||||
<div
|
||||
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-2"
|
||||
>
|
||||
<button
|
||||
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-white hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700 outline outline-1 outline-gray-200 dark:outline-gray-800 rounded-lg transition group"
|
||||
on:click={() => {
|
||||
submitPrompt(prompt.content);
|
||||
}}
|
||||
<div class=" mb-3 md:p-1 text-left w-full">
|
||||
<div class=" flex flex-wrap-reverse px-2 text-left">
|
||||
{#each prompts as prompt, promptIdx}
|
||||
<div
|
||||
class="{promptIdx > 1 ? 'hidden sm:inline-flex' : ''} basis-full sm:basis-1/2 p-[5px] px-1"
|
||||
>
|
||||
<div class="flex flex-col text-left self-center">
|
||||
{#if prompt.title && prompt.title[0] !== ''}
|
||||
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
|
||||
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
|
||||
{:else}
|
||||
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
|
||||
{prompt.content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="self-center p-1 rounded-lg text-white group-hover:bg-gray-100 group-hover:text-gray-800 dark:group-hover:bg-gray-800 dark:group-hover:text-gray-100 dark:text-gray-900 transition"
|
||||
<button
|
||||
class=" flex-1 flex justify-between w-full h-full px-4 py-2.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 rounded-2xl transition group"
|
||||
on:click={() => {
|
||||
submitPrompt(prompt.content);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
<div class="flex flex-col text-left self-center">
|
||||
{#if prompt.title && prompt.title[0] !== ''}
|
||||
<div class="text-sm font-medium dark:text-gray-300">{prompt.title[0]}</div>
|
||||
<div class="text-sm text-gray-500 line-clamp-1">{prompt.title[1]}</div>
|
||||
{:else}
|
||||
<div class=" self-center text-sm font-medium dark:text-gray-300 line-clamp-2">
|
||||
{prompt.content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="self-center p-1 rounded-lg text-gray-50 group-hover:text-gray-800 dark:text-gray-850 dark:group-hover:text-gray-100 transition"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
<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="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>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { chats, config, modelfiles, settings, user } from '$lib/stores';
|
||||
import { tick } from 'svelte';
|
||||
import { tick, getContext } from 'svelte';
|
||||
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { getChatList, updateChatById } from '$lib/apis/chats';
|
||||
|
||||
import UserMessage from './Messages/UserMessage.svelte';
|
||||
import ResponseMessage from './Messages/ResponseMessage.svelte';
|
||||
import Placeholder from './Messages/Placeholder.svelte';
|
||||
import Spinner from '../common/Spinner.svelte';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let chatId = '';
|
||||
export let sendPrompt: Function;
|
||||
@@ -66,7 +69,7 @@
|
||||
navigator.clipboard.writeText(text).then(
|
||||
function () {
|
||||
console.log('Async: Copying to clipboard was successful!');
|
||||
toast.success('Copying to clipboard was successful!');
|
||||
toast.success($i18n.t('Copying to clipboard was successful!'));
|
||||
},
|
||||
function (err) {
|
||||
console.error('Async: Could not copy text: ', err);
|
||||
@@ -221,6 +224,81 @@
|
||||
scrollToBottom();
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const messageDeleteHandler = async (messageId) => {
|
||||
const messageToDelete = history.messages[messageId];
|
||||
const messageParentId = messageToDelete.parentId;
|
||||
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 = [];
|
||||
|
||||
// // Iterate through childrenIds to find grandchildrenIds
|
||||
// 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);
|
||||
// }
|
||||
|
||||
// history.messages[parentId].childrenIds.push(...grandchildrenIds);
|
||||
// history.messages[parentId].childrenIds = history.messages[parentId].childrenIds.filter(
|
||||
// (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;
|
||||
// }
|
||||
|
||||
// await updateChatById(localStorage.token, chatId, { messages, history });
|
||||
// };
|
||||
</script>
|
||||
|
||||
{#if messages.length == 0}
|
||||
@@ -237,8 +315,10 @@
|
||||
>
|
||||
{#if message.role === 'user'}
|
||||
<UserMessage
|
||||
on:delete={() => messageDeleteHandler(message.id)}
|
||||
user={$user}
|
||||
{message}
|
||||
isFirstMessage={messageIdx === 0}
|
||||
siblings={message.parentId !== null
|
||||
? history.messages[message.parentId]?.childrenIds ?? []
|
||||
: Object.values(history.messages)
|
||||
@@ -249,52 +329,6 @@
|
||||
{showNextMessage}
|
||||
{copyToClipboard}
|
||||
/>
|
||||
|
||||
{#if messages.length - 1 === messageIdx && processing !== ''}
|
||||
<div class="flex my-2.5 ml-12 items-center w-fit space-x-2.5">
|
||||
<div class=" dark:text-blue-100">
|
||||
<svg
|
||||
class=" w-4 h-4 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
|
||||
>
|
||||
</div>
|
||||
<div class=" text-sm font-medium">
|
||||
{processing}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<ResponseMessage
|
||||
{message}
|
||||
@@ -308,6 +342,16 @@
|
||||
{copyToClipboard}
|
||||
{continueGeneration}
|
||||
{regenerateResponse}
|
||||
on:save={async (e) => {
|
||||
console.log('save', e);
|
||||
|
||||
const message = e.detail;
|
||||
history.messages[message.id] = message;
|
||||
await updateChatById(localStorage.token, chatId, {
|
||||
messages: messages,
|
||||
history: history
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import { user } from '$lib/stores';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let models = [];
|
||||
export let modelfiles = [];
|
||||
@@ -27,14 +31,16 @@
|
||||
>
|
||||
{#if model in modelfiles}
|
||||
<img
|
||||
src={modelfiles[model]?.imageUrl ?? './favicon.png'}
|
||||
src={modelfiles[model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
alt="modelfile"
|
||||
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
draggable="false"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={models.length === 1 ? '/favicon.png' : '/favicon.png'}
|
||||
src={models.length === 1
|
||||
? `${WEBUI_BASE_URL}/static/favicon.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
class=" w-14 rounded-full border-[1px] border-gray-200 dark:border-none"
|
||||
alt="logo"
|
||||
draggable="false"
|
||||
@@ -44,7 +50,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class=" mt-2 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
|
||||
<div class=" mt-2 mb-5 text-2xl text-gray-800 dark:text-gray-100 font-semibold">
|
||||
{#if modelfile}
|
||||
<span class=" capitalize">
|
||||
{modelfile.title}
|
||||
@@ -60,7 +66,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
How can I help you today?
|
||||
<div class=" line-clamp-1">{$i18n.t('Hello, {{name}}', { name: $user.name })}</div>
|
||||
|
||||
<div>{$i18n.t('How can I help you today?')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,31 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import dayjs from 'dayjs';
|
||||
import { marked } from 'marked';
|
||||
import { settings } from '$lib/stores';
|
||||
import tippy from 'tippy.js';
|
||||
import auto_render from 'katex/dist/contrib/auto-render.mjs';
|
||||
import 'katex/dist/katex.min.css';
|
||||
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { config, settings } from '$lib/stores';
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
||||
import { imageGenerations } from '$lib/apis/images';
|
||||
import { extractSentences } from '$lib/utils';
|
||||
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import Skeleton from './Skeleton.svelte';
|
||||
import CodeBlock from './CodeBlock.svelte';
|
||||
|
||||
import { synthesizeOpenAISpeech } from '$lib/apis/openai';
|
||||
import { extractSentences } from '$lib/utils';
|
||||
import Image from '$lib/components/common/Image.svelte';
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
export let modelfiles = [];
|
||||
export let message;
|
||||
@@ -34,7 +44,7 @@
|
||||
|
||||
let edit = false;
|
||||
let editedContent = '';
|
||||
|
||||
let editTextAreaElement: HTMLTextAreaElement;
|
||||
let tooltipInstance = null;
|
||||
|
||||
let sentencesAudio = {};
|
||||
@@ -42,6 +52,7 @@
|
||||
let speakingIdx = null;
|
||||
|
||||
let loadingSpeech = false;
|
||||
let generatingImage = false;
|
||||
|
||||
$: tokens = marked.lexer(message.content);
|
||||
|
||||
@@ -72,13 +83,20 @@
|
||||
|
||||
if (message.info) {
|
||||
tooltipInstance = tippy(`#info-${message.id}`, {
|
||||
content: `<span class="text-xs" id="tooltip-${message.id}">token/s: ${
|
||||
content: `<span class="text-xs" id="tooltip-${message.id}">response_token/s: ${
|
||||
`${
|
||||
Math.round(
|
||||
((message.info.eval_count ?? 0) / (message.info.eval_duration / 1000000000)) * 100
|
||||
) / 100
|
||||
} tokens` ?? 'N/A'
|
||||
}<br/>
|
||||
prompt_token/s: ${
|
||||
Math.round(
|
||||
((message.info.prompt_eval_count ?? 0) /
|
||||
(message.info.prompt_eval_duration / 1000000000)) *
|
||||
100
|
||||
) / 100 ?? 'N/A'
|
||||
} tokens<br/>
|
||||
total_duration: ${
|
||||
Math.round(((message.info.total_duration ?? 0) / 1000000) * 100) / 100 ??
|
||||
'N/A'
|
||||
@@ -109,10 +127,11 @@
|
||||
// customised options
|
||||
// • auto-render specific keys, e.g.:
|
||||
delimiters: [
|
||||
{ left: '$$', right: '$$', display: true },
|
||||
// { left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: true },
|
||||
{ left: '\\[', right: '\\]', display: true }
|
||||
{ left: '$$', right: '$$', display: false },
|
||||
{ left: '$', right: '$', display: false },
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: false },
|
||||
{ left: '[ ', right: ' ]', display: false }
|
||||
],
|
||||
// • rendering keys, e.g.:
|
||||
throwOnError: false
|
||||
@@ -232,10 +251,9 @@
|
||||
editedContent = message.content;
|
||||
|
||||
await tick();
|
||||
const editElement = document.getElementById(`message-edit-${message.id}`);
|
||||
|
||||
editElement.style.height = '';
|
||||
editElement.style.height = `${editElement.scrollHeight}px`;
|
||||
editTextAreaElement.style.height = '';
|
||||
editTextAreaElement.style.height = `${editTextAreaElement.scrollHeight}px`;
|
||||
};
|
||||
|
||||
const editMessageConfirmHandler = async () => {
|
||||
@@ -259,6 +277,25 @@
|
||||
renderStyling();
|
||||
};
|
||||
|
||||
const generateImage = async (message) => {
|
||||
generatingImage = true;
|
||||
const res = await imageGenerations(localStorage.token, message.content).catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
console.log(res);
|
||||
|
||||
if (res) {
|
||||
message.files = res.map((image) => ({
|
||||
type: 'image',
|
||||
url: `${image.url}`
|
||||
}));
|
||||
|
||||
dispatch('save', message);
|
||||
}
|
||||
|
||||
generatingImage = false;
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
await tick();
|
||||
renderStyling();
|
||||
@@ -267,7 +304,9 @@
|
||||
|
||||
{#key message.id}
|
||||
<div class=" flex w-full message-{message.id}">
|
||||
<ProfileImage src={modelfiles[message.model]?.imageUrl ?? '/favicon.png'} />
|
||||
<ProfileImage
|
||||
src={modelfiles[message.model]?.imageUrl ?? `${WEBUI_BASE_URL}/static/favicon.png`}
|
||||
/>
|
||||
|
||||
<div class="w-full overflow-hidden">
|
||||
<Name>
|
||||
@@ -279,7 +318,7 @@
|
||||
|
||||
{#if message.timestamp}
|
||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
@@ -287,17 +326,31 @@
|
||||
{#if message.content === ''}
|
||||
<Skeleton />
|
||||
{:else}
|
||||
{#if message.files}
|
||||
<div class="my-2.5 w-full flex overflow-x-auto gap-2 flex-wrap">
|
||||
{#each message.files as file}
|
||||
<div>
|
||||
{#if file.type === 'image'}
|
||||
<Image src={file.url} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-li:-mb-4 whitespace-pre-line"
|
||||
class="prose chat-{message.role} w-full max-w-full dark:prose-invert prose-headings:my-0 prose-p:m-0 prose-p:-mb-6 prose-pre:my-0 prose-table:my-0 prose-blockquote:my-0 prose-img:my-0 prose-ul:-my-4 prose-ol:-my-4 prose-li:-my-3 prose-ul:-mb-6 prose-ol:-mb-8 prose-ol:p-0 prose-li:-mb-4 whitespace-pre-line"
|
||||
>
|
||||
<div>
|
||||
{#if edit === true}
|
||||
<div class=" w-full">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={editTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
@@ -309,7 +362,7 @@
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
Save
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -318,7 +371,7 @@
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -366,10 +419,10 @@
|
||||
|
||||
{#if message.done}
|
||||
<div
|
||||
class=" flex justify-start space-x-1 -mt-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
||||
class=" flex justify-start space-x-1 overflow-x-auto buttons text-gray-700 dark:text-gray-500"
|
||||
>
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center min-w-fit">
|
||||
<div class="flex self-center min-w-fit -mt-1">
|
||||
<button
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
@@ -416,257 +469,346 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
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
|
||||
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}
|
||||
<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="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>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
{#if message.info}
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
console.log(message);
|
||||
editMessageHandler();
|
||||
}}
|
||||
id="info-{message.id}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
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"
|
||||
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>
|
||||
|
||||
<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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
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>
|
||||
</Tooltip>
|
||||
|
||||
<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
|
||||
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
|
||||
? '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
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
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}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="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"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</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="2"
|
||||
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 message.info}
|
||||
<Tooltip content="Generation Info" placement="bottom">
|
||||
<button
|
||||
class=" {isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
|
||||
on:click={() => {
|
||||
console.log(message);
|
||||
}}
|
||||
id="info-{message.id}"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
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 isLastMessage}
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
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"
|
||||
<Tooltip content="Continue Response" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={() => {
|
||||
continueGeneration();
|
||||
}}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
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>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
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
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: '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"
|
||||
<Tooltip content="Regenerate" placement="bottom">
|
||||
<button
|
||||
type="button"
|
||||
class="{isLastMessage
|
||||
? 'visible'
|
||||
: 'invisible group-hover:visible'} p-1 rounded dark:hover:text-white hover:text-black transition regenerate-response-button"
|
||||
on:click={regenerateResponse}
|
||||
>
|
||||
<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>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
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}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
<script lang="ts">
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { tick } from 'svelte';
|
||||
import { tick, createEventDispatcher, getContext } from 'svelte';
|
||||
import Name from './Name.svelte';
|
||||
import ProfileImage from './ProfileImage.svelte';
|
||||
import { modelfiles, settings } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let user;
|
||||
export let message;
|
||||
export let siblings;
|
||||
export let isFirstMessage: boolean;
|
||||
|
||||
export let confirmEditMessage: Function;
|
||||
export let showPreviousMessage: Function;
|
||||
@@ -17,18 +23,17 @@
|
||||
|
||||
let edit = false;
|
||||
let editedContent = '';
|
||||
|
||||
let messageEditTextAreaElement: HTMLTextAreaElement;
|
||||
const editMessageHandler = async () => {
|
||||
edit = true;
|
||||
editedContent = message.content;
|
||||
|
||||
await tick();
|
||||
const editElement = document.getElementById(`message-edit-${message.id}`);
|
||||
|
||||
editElement.style.height = '';
|
||||
editElement.style.height = `${editElement.scrollHeight}px`;
|
||||
messageEditTextAreaElement.style.height = '';
|
||||
messageEditTextAreaElement.style.height = `${messageEditTextAreaElement.scrollHeight}px`;
|
||||
|
||||
editElement?.focus();
|
||||
messageEditTextAreaElement?.focus();
|
||||
};
|
||||
|
||||
const editMessageConfirmHandler = async () => {
|
||||
@@ -42,6 +47,10 @@
|
||||
edit = false;
|
||||
editedContent = '';
|
||||
};
|
||||
|
||||
const deleteMessageHandler = async () => {
|
||||
dispatch('delete', message.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class=" flex w-full">
|
||||
@@ -58,17 +67,18 @@
|
||||
{#if $modelfiles.map((modelfile) => modelfile.tagName).includes(message.user)}
|
||||
{$modelfiles.find((modelfile) => modelfile.tagName === message.user)?.title}
|
||||
{:else}
|
||||
You <span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
||||
{$i18n.t('You')}
|
||||
<span class=" text-gray-500 text-sm font-medium">{message?.user ?? ''}</span>
|
||||
{/if}
|
||||
{:else if $settings.showUsername}
|
||||
{user.name}
|
||||
{:else}
|
||||
You
|
||||
{$i18n.t('You')}
|
||||
{/if}
|
||||
|
||||
{#if message.timestamp}
|
||||
<span class=" invisible group-hover:visible text-gray-400 text-xs font-medium">
|
||||
{dayjs(message.timestamp * 1000).format('DD/MM/YYYY HH:mm')}
|
||||
{dayjs(message.timestamp * 1000).format($i18n.t('DD/MM/YYYY HH:mm'))}
|
||||
</span>
|
||||
{/if}
|
||||
</Name>
|
||||
@@ -116,7 +126,7 @@
|
||||
{file.name}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">Document</div>
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Document')}</div>
|
||||
</div>
|
||||
</button>
|
||||
{:else if file.type === 'collection'}
|
||||
@@ -145,7 +155,7 @@
|
||||
{file?.title ?? `#${file.name}`}
|
||||
</div>
|
||||
|
||||
<div class=" text-gray-500 text-sm">Collection</div>
|
||||
<div class=" text-gray-500 text-sm">{$i18n.t('Collection')}</div>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -158,9 +168,11 @@
|
||||
<div class=" w-full">
|
||||
<textarea
|
||||
id="message-edit-{message.id}"
|
||||
bind:this={messageEditTextAreaElement}
|
||||
class=" bg-transparent outline-none w-full resize-none"
|
||||
bind:value={editedContent}
|
||||
on:input={(e) => {
|
||||
e.target.style.height = '';
|
||||
e.target.style.height = `${e.target.scrollHeight}px`;
|
||||
}}
|
||||
/>
|
||||
@@ -172,7 +184,7 @@
|
||||
editMessageConfirmHandler();
|
||||
}}
|
||||
>
|
||||
Save & Submit
|
||||
{$i18n.t('Save & Submit')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -181,7 +193,7 @@
|
||||
cancelEditMessage();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
{$i18n.t('Cancel')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,11 +201,11 @@
|
||||
<div class="w-full">
|
||||
<pre id="user-message">{message.content}</pre>
|
||||
|
||||
<div class=" flex justify-start space-x-1">
|
||||
<div class=" flex justify-start space-x-1 text-gray-700 dark:text-gray-500">
|
||||
{#if siblings.length > 1}
|
||||
<div class="flex self-center">
|
||||
<div class="flex self-center -mt-1">
|
||||
<button
|
||||
class="self-center"
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showPreviousMessage(message);
|
||||
}}
|
||||
@@ -212,12 +224,12 @@
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div class="text-xs font-bold self-center">
|
||||
<div class="text-xs font-bold self-center dark:text-gray-100">
|
||||
{siblings.indexOf(message.id) + 1} / {siblings.length}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="self-center"
|
||||
class="self-center dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
showNextMessage(message);
|
||||
}}
|
||||
@@ -238,49 +250,79 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white transition edit-user-message-button"
|
||||
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"
|
||||
<Tooltip content="Edit" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition edit-user-message-button"
|
||||
on:click={() => {
|
||||
editMessageHandler();
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
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>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white 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"
|
||||
<Tooltip content="Copy" placement="bottom">
|
||||
<button
|
||||
class="invisible group-hover:visible p-1 rounded dark:hover:text-white hover:text-black transition"
|
||||
on:click={() => {
|
||||
copyToClipboard(message.content);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
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>
|
||||
</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="2"
|
||||
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}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { setDefaultModels } from '$lib/apis/configs';
|
||||
import { models, showSettings, settings, user } from '$lib/stores';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let selectedModels = [''];
|
||||
export let disabled = false;
|
||||
@@ -10,7 +12,7 @@
|
||||
const saveDefaultModel = async () => {
|
||||
const hasEmptyModel = selectedModels.filter((it) => it === '');
|
||||
if (hasEmptyModel.length) {
|
||||
toast.error('Choose a model before saving...');
|
||||
toast.error($i18n.t('Choose a model before saving...'));
|
||||
return;
|
||||
}
|
||||
settings.set({ ...$settings, models: selectedModels });
|
||||
@@ -20,12 +22,12 @@
|
||||
console.log('setting default models globally');
|
||||
await setDefaultModels(localStorage.token, selectedModels.join(','));
|
||||
}
|
||||
toast.success('Default model updated');
|
||||
toast.success($i18n.t('Default model updated'));
|
||||
};
|
||||
|
||||
$: if (selectedModels.length > 0 && $models.length > 0) {
|
||||
selectedModels = selectedModels.map((model) =>
|
||||
$models.map((m) => m.name).includes(model) ? model : ''
|
||||
$models.map((m) => m.id).includes(model) ? model : ''
|
||||
);
|
||||
}
|
||||
</script>
|
||||
@@ -39,13 +41,15 @@
|
||||
bind:value={selectedModel}
|
||||
{disabled}
|
||||
>
|
||||
<option class=" text-gray-700" value="" selected disabled>Select a model</option>
|
||||
<option class=" text-gray-700" value="" selected disabled
|
||||
>{$i18n.t('Select a model')}</option
|
||||
>
|
||||
|
||||
{#each $models as model}
|
||||
{#if model.name === 'hr'}
|
||||
<hr />
|
||||
{:else}
|
||||
<option value={model.name} class="text-gray-700 text-lg"
|
||||
<option value={model.id} class="text-gray-700 text-lg"
|
||||
>{model.name +
|
||||
`${model.size ? ` (${(model.size / 1024 ** 3).toFixed(1)}GB)` : ''}`}</option
|
||||
>
|
||||
@@ -133,5 +137,5 @@
|
||||
</div>
|
||||
|
||||
<div class="text-left mt-1.5 text-xs text-gray-500">
|
||||
<button on:click={saveDefaultModel}> Set as default</button>
|
||||
<button on:click={saveDefaultModel}> {$i18n.t('Set as default')}</button>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,104 @@
|
||||
<script lang="ts">
|
||||
import { getVersionUpdates } from '$lib/apis';
|
||||
import { getOllamaVersion } from '$lib/apis/ollama';
|
||||
import { WEBUI_NAME, WEB_UI_VERSION } from '$lib/constants';
|
||||
import { config } from '$lib/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { WEBUI_VERSION } from '$lib/constants';
|
||||
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
|
||||
import { compareVersion } from '$lib/utils';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let ollamaVersion = '';
|
||||
|
||||
let updateAvailable = null;
|
||||
let version = {
|
||||
current: '',
|
||||
latest: ''
|
||||
};
|
||||
|
||||
const checkForVersionUpdates = async () => {
|
||||
updateAvailable = null;
|
||||
version = await getVersionUpdates(localStorage.token).catch((error) => {
|
||||
return {
|
||||
current: WEBUI_VERSION,
|
||||
latest: WEBUI_VERSION
|
||||
};
|
||||
});
|
||||
|
||||
console.log(version);
|
||||
|
||||
updateAvailable = compareVersion(version.latest, version.current);
|
||||
console.log(updateAvailable);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
|
||||
return '';
|
||||
});
|
||||
|
||||
checkForVersionUpdates();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
|
||||
<div class=" space-y-3">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{WEBUI_NAME} Version</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||
{$config && $config.version ? $config.version : WEB_UI_VERSION}
|
||||
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
||||
<div>
|
||||
{$WEBUI_NAME}
|
||||
{$i18n.t('Version')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
||||
<div>
|
||||
v{WEBUI_VERSION}
|
||||
|
||||
<a
|
||||
href="https://github.com/open-webui/open-webui/releases/tag/v{version.latest}"
|
||||
target="_blank"
|
||||
>
|
||||
{updateAvailable === null
|
||||
? $i18n.t('Checking for updates...')
|
||||
: updateAvailable
|
||||
? `(v${version.latest} ${$i18n.t('available!')})`
|
||||
: $i18n.t('(latest)')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
|
||||
on:click={() => {
|
||||
showChangelog.set(true);
|
||||
}}
|
||||
>
|
||||
<div>{$i18n.t("See what's new")}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
{#if ollamaVersion}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Ollama Version</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||
{ollamaVersion ?? 'N/A'}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Version')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||
{ollamaVersion ?? 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
@@ -44,16 +110,24 @@
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/ollama-webui/ollama-webui" target="_blank">
|
||||
<a href="https://twitter.com/OpenWebUI" target="_blank">
|
||||
<img
|
||||
alt="X (formerly Twitter) Follow"
|
||||
src="https://img.shields.io/twitter/follow/OpenWebUI"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/open-webui/open-webui" target="_blank">
|
||||
<img
|
||||
alt="Github Repo"
|
||||
src="https://img.shields.io/github/stars/ollama-webui/ollama-webui?style=social&label=Star us on Github"
|
||||
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Created by <a
|
||||
{$i18n.t('Created by')}
|
||||
<a
|
||||
class=" text-gray-500 dark:text-gray-300 font-medium"
|
||||
href="https://github.com/tjbck"
|
||||
target="_blank">Timothy J. Baek</a
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
import { updateUserProfile } from '$lib/apis/auths';
|
||||
|
||||
import UpdatePassword from './Account/UpdatePassword.svelte';
|
||||
import { getGravatarUrl } from '$lib/apis/utils';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let profileImageUrl = '';
|
||||
let name = '';
|
||||
let showJWTToken = false;
|
||||
let JWTTokenCopied = false;
|
||||
let profileImageInputElement: HTMLInputElement;
|
||||
|
||||
const submitHandler = async () => {
|
||||
const updatedUser = await updateUserProfile(localStorage.token, name, profileImageUrl).catch(
|
||||
@@ -34,14 +40,15 @@
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<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-[22rem]">
|
||||
<input
|
||||
id="profile-image-input"
|
||||
bind:this={profileImageInputElement}
|
||||
type="file"
|
||||
hidden
|
||||
accept="image/*"
|
||||
on:change={(e) => {
|
||||
const files = e?.target?.files ?? [];
|
||||
const files = profileImageInputElement.files ?? [];
|
||||
let reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
let originalImageUrl = `${event.target.result}`;
|
||||
@@ -83,7 +90,7 @@
|
||||
// Display the compressed image
|
||||
profileImageUrl = compressedSrc;
|
||||
|
||||
e.target.files = null;
|
||||
profileImageInputElement.files = null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -96,7 +103,7 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" mb-2.5 text-sm font-medium">Profile</div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Profile')}</div>
|
||||
|
||||
<div class="flex space-x-5">
|
||||
<div class="flex flex-col">
|
||||
@@ -105,7 +112,7 @@
|
||||
class="relative rounded-full dark:bg-gray-700"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('profile-image-input')?.click();
|
||||
profileImageInputElement.click();
|
||||
}}
|
||||
>
|
||||
<img
|
||||
@@ -138,13 +145,13 @@
|
||||
const url = await getGravatarUrl($user.email);
|
||||
|
||||
profileImageUrl = url;
|
||||
}}>Use Gravatar</button
|
||||
}}>{$i18n.t('Use Gravatar')}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">Name</div>
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
@@ -160,11 +167,113 @@
|
||||
|
||||
<hr class=" dark:border-gray-700 my-4" />
|
||||
<UpdatePassword />
|
||||
|
||||
<hr class=" dark:border-gray-700 my-4" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('JWT Token')}</div>
|
||||
</div>
|
||||
|
||||
<div class="flex mt-2">
|
||||
<div class="flex w-full">
|
||||
<input
|
||||
class="w-full rounded-l-lg py-1.5 pl-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
type={showJWTToken ? 'text' : 'password'}
|
||||
value={localStorage.token}
|
||||
disabled
|
||||
/>
|
||||
|
||||
<button
|
||||
class="dark:bg-gray-800 px-2 transition rounded-r-lg"
|
||||
on:click={() => {
|
||||
showJWTToken = !showJWTToken;
|
||||
}}
|
||||
>
|
||||
{#if showJWTToken}
|
||||
<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="M3.28 2.22a.75.75 0 0 0-1.06 1.06l10.5 10.5a.75.75 0 1 0 1.06-1.06l-1.322-1.323a7.012 7.012 0 0 0 2.16-3.11.87.87 0 0 0 0-.567A7.003 7.003 0 0 0 4.82 3.76l-1.54-1.54Zm3.196 3.195 1.135 1.136A1.502 1.502 0 0 1 9.45 8.389l1.136 1.135a3 3 0 0 0-4.109-4.109Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
d="m7.812 10.994 1.816 1.816A7.003 7.003 0 0 1 1.38 8.28a.87.87 0 0 1 0-.566 6.985 6.985 0 0 1 1.113-2.039l2.513 2.513a3 3 0 0 0 2.806 2.806Z"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M1.38 8.28a.87.87 0 0 1 0-.566 7.003 7.003 0 0 1 13.238.006.87.87 0 0 1 0 .566A7.003 7.003 0 0 1 1.379 8.28ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="ml-1.5 px-1.5 py-1 hover:bg-gray-800 transition rounded-lg"
|
||||
on:click={() => {
|
||||
copyToClipboard(localStorage.token);
|
||||
JWTTokenCopied = true;
|
||||
setTimeout(() => {
|
||||
JWTTokenCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if JWTTokenCopied}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<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="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
on:click={async () => {
|
||||
const res = await submitHandler();
|
||||
|
||||
@@ -173,7 +282,7 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { updateUserPassword } from '$lib/apis/auths';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let show = false;
|
||||
let currentPassword = '';
|
||||
let newPassword = '';
|
||||
@@ -17,7 +20,7 @@
|
||||
);
|
||||
|
||||
if (res) {
|
||||
toast.success('Successfully updated.');
|
||||
toast.success($i18n.t('Successfully updated.'));
|
||||
}
|
||||
|
||||
currentPassword = '';
|
||||
@@ -39,21 +42,21 @@
|
||||
updatePasswordHandler();
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-between mb-2.5 items-center text-sm">
|
||||
<div class=" font-medium">Change Password</div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Change Password')}</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
}}>{show ? 'Hide' : 'Show'}</button
|
||||
}}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if show}
|
||||
<div class=" space-y-1.5">
|
||||
<div class=" py-2.5 space-y-1.5">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">Current Password</div>
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Current Password')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
@@ -67,7 +70,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">New Password</div>
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
@@ -81,7 +84,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">Confirm Password</div>
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Confirm Password')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
@@ -99,7 +102,7 @@
|
||||
<button
|
||||
class=" px-4 py-2 text-xs bg-gray-800 hover:bg-gray-900 dark:bg-gray-700 dark:hover:bg-gray-800 text-gray-100 transition rounded-md font-medium"
|
||||
>
|
||||
Update password
|
||||
{$i18n.t('Update password')}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||
export let saveSettings: Function;
|
||||
|
||||
// Advanced
|
||||
@@ -55,14 +57,14 @@
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div class=" text-sm font-medium">Parameters</div>
|
||||
<div class=" text-sm font-medium">{$i18n.t('Parameters')}</div>
|
||||
|
||||
<AdvancedParams bind:options />
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class=" py-1 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Keep Alive</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -72,9 +74,9 @@
|
||||
}}
|
||||
>
|
||||
{#if keepAlive === null}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -84,7 +86,7 @@
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
type="text"
|
||||
placeholder={`e.g.) "30s","10m". Valid time units are "s", "m", "h".`}
|
||||
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
||||
bind:value={keepAlive}
|
||||
/>
|
||||
</div>
|
||||
@@ -93,7 +95,7 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-1 flex w-full justify-between">
|
||||
<div class=" self-center text-sm font-medium">Request Mode</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -102,7 +104,7 @@
|
||||
}}
|
||||
>
|
||||
{#if requestFormat === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||
{:else if requestFormat === 'json'}
|
||||
<!-- <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -114,7 +116,7 @@
|
||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||
/>
|
||||
</svg> -->
|
||||
<span class="ml-2 self-center"> JSON </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('JSON')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -147,7 +149,7 @@
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
Save
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let options = {
|
||||
// Advanced
|
||||
seed: 0,
|
||||
@@ -20,7 +24,7 @@
|
||||
<div class=" space-y-3 text-xs">
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" w-20 text-xs font-medium self-center">Seed</div>
|
||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Seed')}</div>
|
||||
<div class=" flex-1 self-center">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
@@ -36,12 +40,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" w-20 text-xs font-medium self-center">Stop Sequence</div>
|
||||
<div class=" w-20 text-xs font-medium self-center">{$i18n.t('Stop Sequence')}</div>
|
||||
<div class=" flex-1 self-center">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
type="text"
|
||||
placeholder="Enter Stop Sequence"
|
||||
placeholder={$i18n.t('Enter stop sequence')}
|
||||
bind:value={options.stop}
|
||||
autocomplete="off"
|
||||
/>
|
||||
@@ -51,7 +55,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Temperature</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Temperature')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -61,9 +65,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.temperature === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -97,7 +101,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Mirostat</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -107,9 +111,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.mirostat === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -143,7 +147,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Mirostat Eta</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Eta')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -153,9 +157,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.mirostat_eta === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -189,7 +193,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Mirostat Tau</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Mirostat Tau')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -199,9 +203,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.mirostat_tau === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -235,7 +239,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Top K</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Top K')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -245,9 +249,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.top_k === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -281,7 +285,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Top P</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Top P')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -291,9 +295,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.top_p === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -327,7 +331,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Repeat Penalty</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Penalty')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -337,9 +341,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.repeat_penalty === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -373,7 +377,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Repeat Last N</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Repeat Last N')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -383,9 +387,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.repeat_last_n === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -419,7 +423,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Tfs Z</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Tfs Z')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -429,9 +433,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.tfs_z === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -465,7 +469,7 @@
|
||||
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Context Length</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Context Length')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -475,9 +479,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.num_ctx === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -510,7 +514,7 @@
|
||||
</div>
|
||||
<div class=" py-0.5 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Max Tokens</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Max Tokens')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -520,9 +524,9 @@
|
||||
}}
|
||||
>
|
||||
{#if options.num_predict === ''}
|
||||
<span class="ml-2 self-center"> Default </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> Custom </span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
// Audio
|
||||
@@ -101,32 +103,36 @@
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">STT Settings</div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Speech-to-Text Engine</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={STTEngine}
|
||||
placeholder="Select a mode"
|
||||
on:change={(e) => {
|
||||
if (e.target.value !== '') {
|
||||
navigator.mediaDevices.getUserMedia({ audio: true }).catch(function (err) {
|
||||
toast.error(`Permission denied when accessing microphone: ${err}`);
|
||||
toast.error(
|
||||
$i18n.t(`Permission denied when accessing microphone: {{error}}`, {
|
||||
error: err
|
||||
})
|
||||
);
|
||||
STTEngine = '';
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Default (Web API)</option>
|
||||
<option value="whisper-local">Whisper (Local)</option>
|
||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
||||
<option value="whisper-local">{$i18n.t('Whisper (Local)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Conversation Mode</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Conversation Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -136,15 +142,17 @@
|
||||
type="button"
|
||||
>
|
||||
{#if conversationMode === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Auto-send input after 3 sec.</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Auto-send input after 3 sec.')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -154,22 +162,22 @@
|
||||
type="button"
|
||||
>
|
||||
{#if speechAutoSend === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">TTS Settings</div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Text-to-Speech Engine</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={TTSEngine}
|
||||
placeholder="Select a mode"
|
||||
on:change={(e) => {
|
||||
@@ -182,14 +190,14 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<option value="">Default (Web API)</option>
|
||||
<option value="openai">Open AI</option>
|
||||
<option value="">{$i18n.t('Default (Web API)')}</option>
|
||||
<option value="openai">{$i18n.t('Open AI')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Auto-playback response</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -199,9 +207,9 @@
|
||||
type="button"
|
||||
>
|
||||
{#if responseAutoPlayback === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -211,7 +219,7 @@
|
||||
|
||||
{#if TTSEngine === ''}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Set Voice</div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
@@ -219,7 +227,7 @@
|
||||
bind:value={speaker}
|
||||
placeholder="Select a voice"
|
||||
>
|
||||
<option value="" selected>Default</option>
|
||||
<option value="" selected>{$i18n.t('Default')}</option>
|
||||
{#each voices.filter((v) => v.localService === true) as voice}
|
||||
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option
|
||||
>
|
||||
@@ -230,7 +238,7 @@
|
||||
</div>
|
||||
{:else if TTSEngine === 'openai'}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Set Voice</div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
@@ -251,10 +259,10 @@
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -13,15 +13,18 @@
|
||||
getChatList
|
||||
} from '$lib/apis/chats';
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveSettings: Function;
|
||||
// Chats
|
||||
let saveChatHistory = true;
|
||||
let importFiles;
|
||||
let showDeleteConfirm = false;
|
||||
let chatImportInputElement: HTMLInputElement;
|
||||
|
||||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
@@ -75,7 +78,9 @@
|
||||
|
||||
const deleteChats = async () => {
|
||||
await goto('/');
|
||||
await deleteAllChats(localStorage.token);
|
||||
await deleteAllChats(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
};
|
||||
|
||||
@@ -96,13 +101,13 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm max-h-[22rem]">
|
||||
<div class=" space-y-2">
|
||||
<div
|
||||
class="flex flex-col justify-between rounded-md items-center py-2 px-3.5 w-full transition"
|
||||
>
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-sm font-medium">Chat History</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Chat History')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -126,7 +131,7 @@
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-2 self-center"> On </span>
|
||||
<span class="ml-2 self-center"> {$i18n.t('On')} </span>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -144,24 +149,31 @@
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-left w-full font-medium mt-0.5">
|
||||
This setting does not sync across browsers or devices.
|
||||
{$i18n.t('This setting does not sync across browsers or devices.')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<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
|
||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => {
|
||||
document.getElementById('chat-import-input').click();
|
||||
chatImportInputElement.click();
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-3">
|
||||
@@ -178,7 +190,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Import Chats</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Import Chats')}</div>
|
||||
</button>
|
||||
<button
|
||||
class=" flex rounded-md py-2 px-3.5 w-full hover:bg-gray-200 dark:hover:bg-gray-800 transition"
|
||||
@@ -200,7 +212,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Export Chats</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Export Chats')}</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -222,7 +234,7 @@
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<span>Are you sure?</span>
|
||||
<span>{$i18n.t('Are you sure?')}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1.5 items-center">
|
||||
@@ -286,7 +298,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Delete Chats</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Delete Chats')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -314,7 +326,9 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Export All Chats (All Users)</div>
|
||||
<div class=" self-center text-sm font-medium">
|
||||
{$i18n.t('Export All Chats (All Users)')}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
@@ -328,7 +342,7 @@
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success('Success');
|
||||
toast.success($i18n.t('Success'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -346,7 +360,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center text-sm font-medium">Reset Vector Storage</div>
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Reset Vector Storage')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
259
src/lib/components/chat/Settings/Connections.svelte
Normal file
259
src/lib/components/chat/Settings/Connections.svelte
Normal file
@@ -0,0 +1,259 @@
|
||||
<script lang="ts">
|
||||
import { models, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { getOllamaUrls, getOllamaVersion, updateOllamaUrls } from '$lib/apis/ollama';
|
||||
import {
|
||||
getOpenAIKeys,
|
||||
getOpenAIUrls,
|
||||
updateOpenAIKeys,
|
||||
updateOpenAIUrls
|
||||
} from '$lib/apis/openai';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let getModels: Function;
|
||||
|
||||
// External
|
||||
let OLLAMA_BASE_URL = '';
|
||||
let OLLAMA_BASE_URLS = [''];
|
||||
|
||||
let OPENAI_API_KEY = '';
|
||||
let OPENAI_API_BASE_URL = '';
|
||||
|
||||
let OPENAI_API_KEYS = [''];
|
||||
let OPENAI_API_BASE_URLS = [''];
|
||||
|
||||
let showOpenAI = false;
|
||||
|
||||
const updateOpenAIHandler = async () => {
|
||||
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
|
||||
OPENAI_API_KEYS = await updateOpenAIKeys(localStorage.token, OPENAI_API_KEYS);
|
||||
|
||||
await models.set(await getModels());
|
||||
};
|
||||
|
||||
const updateOllamaUrlsHandler = async () => {
|
||||
OLLAMA_BASE_URLS = await updateOllamaUrls(localStorage.token, OLLAMA_BASE_URLS);
|
||||
|
||||
const ollamaVersion = await getOllamaVersion(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (ollamaVersion) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
await models.set(await getModels());
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
|
||||
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
|
||||
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateOpenAIHandler();
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" pr-1.5 overflow-y-scroll max-h-[22rem] space-y-3">
|
||||
<div class=" space-y-3">
|
||||
<div class="mt-2 space-y-2 pr-1.5">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showOpenAI = !showOpenAI;
|
||||
}}>{showOpenAI ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if showOpenAI}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={url}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={OPENAI_API_KEYS[idx]}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="self-center flex items-center">
|
||||
{#if idx === 0}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
|
||||
OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
|
||||
}}
|
||||
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={() => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
}}
|
||||
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>
|
||||
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('WebUI will make requests to')}
|
||||
<span class=" text-gray-200">'{url}/models'</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Ollama Base URL')}</div>
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<div class="flex gap-1.5">
|
||||
<input
|
||||
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 class="">
|
||||
<button
|
||||
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={() => {
|
||||
updateOllamaUrlsHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<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 class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Trouble accessing Ollama?')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Click here for help.')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { getOpenAIKey, getOpenAIUrl, updateOpenAIKey, updateOpenAIUrl } from '$lib/apis/openai';
|
||||
import { models, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let getModels: Function;
|
||||
|
||||
// External
|
||||
let OPENAI_API_KEY = '';
|
||||
let OPENAI_API_BASE_URL = '';
|
||||
|
||||
const updateOpenAIHandler = async () => {
|
||||
OPENAI_API_BASE_URL = await updateOpenAIUrl(localStorage.token, OPENAI_API_BASE_URL);
|
||||
OPENAI_API_KEY = await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
||||
|
||||
await models.set(await getModels());
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
OPENAI_API_BASE_URL = await getOpenAIUrl(localStorage.token);
|
||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateOpenAIHandler();
|
||||
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=" space-y-3">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">OpenAI API Key</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter OpenAI API Key"
|
||||
bind:value={OPENAI_API_KEY}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Adds optional support for online models.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">OpenAI API Base URL</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter OpenAI API Base URL"
|
||||
bind:value={OPENAI_API_BASE_URL}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
WebUI will make requests to <span class=" text-gray-200">'{OPENAI_API_BASE_URL}/chat'</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,33 +1,28 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { getLanguages } from '$lib/i18n';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { getOllamaAPIUrl, updateOllamaAPIUrl } from '$lib/apis/ollama';
|
||||
import { models, user } from '$lib/stores';
|
||||
import { models, user, theme } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||
|
||||
export let saveSettings: Function;
|
||||
export let getModels: Function;
|
||||
|
||||
// General
|
||||
let API_BASE_URL = '';
|
||||
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light'];
|
||||
let theme = 'dark';
|
||||
let selectedTheme = 'system';
|
||||
|
||||
let languages = [];
|
||||
let lang = $i18n.language;
|
||||
let notificationEnabled = false;
|
||||
let system = '';
|
||||
|
||||
const toggleTheme = async () => {
|
||||
if (theme === 'dark') {
|
||||
theme = 'light';
|
||||
} else {
|
||||
theme = 'dark';
|
||||
}
|
||||
|
||||
localStorage.theme = theme;
|
||||
|
||||
document.documentElement.classList.remove(theme === 'dark' ? 'light' : 'dark');
|
||||
document.documentElement.classList.add(theme);
|
||||
};
|
||||
let showAdvanced = false;
|
||||
|
||||
const toggleNotification = async () => {
|
||||
const permission = await Notification.requestPermission();
|
||||
@@ -42,183 +37,280 @@
|
||||
}
|
||||
};
|
||||
|
||||
const updateOllamaAPIUrlHandler = async () => {
|
||||
API_BASE_URL = await updateOllamaAPIUrl(localStorage.token, API_BASE_URL);
|
||||
const _models = await getModels('ollama');
|
||||
// Advanced
|
||||
let requestFormat = '';
|
||||
let keepAlive = null;
|
||||
|
||||
if (_models.length > 0) {
|
||||
toast.success('Server connection verified');
|
||||
await models.set(_models);
|
||||
let options = {
|
||||
// Advanced
|
||||
seed: 0,
|
||||
temperature: '',
|
||||
repeat_penalty: '',
|
||||
repeat_last_n: '',
|
||||
mirostat: '',
|
||||
mirostat_eta: '',
|
||||
mirostat_tau: '',
|
||||
top_k: '',
|
||||
top_p: '',
|
||||
stop: '',
|
||||
tfs_z: '',
|
||||
num_ctx: '',
|
||||
num_predict: ''
|
||||
};
|
||||
|
||||
const toggleRequestFormat = async () => {
|
||||
if (requestFormat === '') {
|
||||
requestFormat = 'json';
|
||||
} else {
|
||||
requestFormat = '';
|
||||
}
|
||||
|
||||
saveSettings({ requestFormat: requestFormat !== '' ? requestFormat : undefined });
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
API_BASE_URL = await getOllamaAPIUrl(localStorage.token);
|
||||
}
|
||||
selectedTheme = localStorage.theme ?? 'system';
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
languages = await getLanguages();
|
||||
|
||||
theme = localStorage.theme ?? 'dark';
|
||||
notificationEnabled = settings.notificationEnabled ?? false;
|
||||
system = settings.system ?? '';
|
||||
|
||||
requestFormat = settings.requestFormat ?? '';
|
||||
keepAlive = settings.keepAlive ?? null;
|
||||
|
||||
options.seed = settings.seed ?? 0;
|
||||
options.temperature = settings.temperature ?? '';
|
||||
options.repeat_penalty = settings.repeat_penalty ?? '';
|
||||
options.top_k = settings.top_k ?? '';
|
||||
options.top_p = settings.top_p ?? '';
|
||||
options.num_ctx = settings.num_ctx ?? '';
|
||||
options = { ...options, ...settings.options };
|
||||
options.stop = (settings?.options?.stop ?? []).join(',');
|
||||
});
|
||||
|
||||
const applyTheme = (_theme: string) => {
|
||||
let themeToApply = _theme;
|
||||
|
||||
if (_theme === 'system') {
|
||||
themeToApply = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
||||
}
|
||||
|
||||
themes
|
||||
.filter((e) => e !== themeToApply)
|
||||
.forEach((e) => {
|
||||
e.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.remove(e);
|
||||
});
|
||||
});
|
||||
|
||||
themeToApply.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.add(e);
|
||||
});
|
||||
|
||||
console.log(_theme);
|
||||
};
|
||||
|
||||
const themeChangeHandler = (_theme: string) => {
|
||||
theme.set(_theme);
|
||||
localStorage.setItem('theme', _theme);
|
||||
|
||||
applyTheme(_theme);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col space-y-3">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">WebUI Settings</div>
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" pr-1.5 overflow-y-scroll max-h-[22rem]">
|
||||
<div class="">
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Theme</div>
|
||||
<div class="flex items-center relative">
|
||||
<div class=" absolute right-16">
|
||||
{#if theme === 'dark'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else if theme === 'light'}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4 self-center"
|
||||
>
|
||||
<path
|
||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<select
|
||||
class="w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={theme}
|
||||
placeholder="Select a theme"
|
||||
on:change={(e) => {
|
||||
localStorage.theme = theme;
|
||||
|
||||
themes
|
||||
.filter((e) => e !== theme)
|
||||
.forEach((e) => {
|
||||
e.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.remove(e);
|
||||
});
|
||||
});
|
||||
|
||||
theme.split(' ').forEach((e) => {
|
||||
document.documentElement.classList.add(e);
|
||||
});
|
||||
|
||||
console.log(theme);
|
||||
}}
|
||||
>
|
||||
<option value="dark">Dark</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="rose-pine dark">Rosé Pine</option>
|
||||
<option value="rose-pine-dawn light">Rosé Pine Dawn</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Notification</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleNotification();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if notificationEnabled === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Ollama API URL</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 mr-2">
|
||||
<input
|
||||
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none"
|
||||
placeholder="Enter URL (e.g. http://localhost:11434/api)"
|
||||
bind:value={API_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded transition"
|
||||
on:click={() => {
|
||||
updateOllamaAPIUrlHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={selectedTheme}
|
||||
placeholder="Select a theme"
|
||||
on:change={() => themeChangeHandler(selectedTheme)}
|
||||
>
|
||||
<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>
|
||||
<option value="system">⚙️ {$i18n.t('System')}</option>
|
||||
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
||||
<option value="light">☀️ {$i18n.t('Light')}</option>
|
||||
<option value="rose-pine dark">🪻 {$i18n.t('Rosé Pine')}</option>
|
||||
<option value="rose-pine-dawn light">🌷 {$i18n.t('Rosé Pine Dawn')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Trouble accessing Ollama?
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/ollama-webui/ollama-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
Click here for help.
|
||||
</a>
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class=" dark:bg-gray-900 w-fit pr-8 rounded py-2 px-2 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={lang}
|
||||
placeholder="Select a language"
|
||||
on:change={(e) => {
|
||||
$i18n.changeLanguage(lang);
|
||||
}}
|
||||
>
|
||||
{#each languages as language}
|
||||
<option value={language['code']}>{language['title']}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{#if $i18n.language === 'en-US'}
|
||||
<div class="mb-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Couldn't find your language?
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
|
||||
target="_blank"
|
||||
>
|
||||
Help us translate Open WebUI!
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Desktop Notifications')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleNotification();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if notificationEnabled === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">System Prompt</div>
|
||||
<textarea
|
||||
bind:value={system}
|
||||
class="w-full rounded p-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
<div>
|
||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||
<textarea
|
||||
bind:value={system}
|
||||
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 space-y-3 pr-1.5">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Advanced Parameters')}</div>
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAdvanced = !showAdvanced;
|
||||
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
{#if showAdvanced}
|
||||
<AdvancedParams bind:options />
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class=" py-1 w-full justify-between">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
keepAlive = keepAlive === null ? '5m' : null;
|
||||
}}
|
||||
>
|
||||
{#if keepAlive === null}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if keepAlive !== null}
|
||||
<div class="flex mt-1 space-x-2">
|
||||
<input
|
||||
class="w-full rounded py-1.5 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none border border-gray-100 dark:border-gray-600"
|
||||
type="text"
|
||||
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
|
||||
bind:value={keepAlive}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-1 flex w-full justify-between">
|
||||
<div class=" self-center text-sm font-medium">{$i18n.t('Request Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleRequestFormat();
|
||||
}}
|
||||
>
|
||||
{#if requestFormat === ''}
|
||||
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
|
||||
{:else if requestFormat === 'json'}
|
||||
<!-- <svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4 self-center"
|
||||
>
|
||||
<path
|
||||
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
|
||||
/>
|
||||
</svg> -->
|
||||
<span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
on:click={() => {
|
||||
saveSettings({
|
||||
system: system !== '' ? system : undefined
|
||||
system: system !== '' ? system : undefined,
|
||||
options: {
|
||||
seed: (options.seed !== 0 ? options.seed : undefined) ?? undefined,
|
||||
stop: options.stop !== '' ? options.stop.split(',').filter((e) => e) : undefined,
|
||||
temperature: options.temperature !== '' ? options.temperature : undefined,
|
||||
repeat_penalty: options.repeat_penalty !== '' ? options.repeat_penalty : undefined,
|
||||
repeat_last_n: options.repeat_last_n !== '' ? options.repeat_last_n : undefined,
|
||||
mirostat: options.mirostat !== '' ? options.mirostat : undefined,
|
||||
mirostat_eta: options.mirostat_eta !== '' ? options.mirostat_eta : undefined,
|
||||
mirostat_tau: options.mirostat_tau !== '' ? options.mirostat_tau : undefined,
|
||||
top_k: options.top_k !== '' ? options.top_k : undefined,
|
||||
top_p: options.top_p !== '' ? options.top_p : undefined,
|
||||
tfs_z: options.tfs_z !== '' ? options.tfs_z : undefined,
|
||||
num_ctx: options.num_ctx !== '' ? options.num_ctx : undefined,
|
||||
num_predict: options.num_predict !== '' ? options.num_predict : undefined
|
||||
},
|
||||
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined
|
||||
});
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
Save
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
343
src/lib/components/chat/Settings/Images.svelte
Normal file
343
src/lib/components/chat/Settings/Images.svelte
Normal file
@@ -0,0 +1,343 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { config, user } from '$lib/stores';
|
||||
import {
|
||||
getAUTOMATIC1111Url,
|
||||
getImageGenerationModels,
|
||||
getDefaultImageGenerationModel,
|
||||
updateDefaultImageGenerationModel,
|
||||
getImageSize,
|
||||
getImageGenerationConfig,
|
||||
updateImageGenerationConfig,
|
||||
updateAUTOMATIC1111Url,
|
||||
updateImageSize,
|
||||
getImageSteps,
|
||||
updateImageSteps,
|
||||
getOpenAIKey,
|
||||
updateOpenAIKey
|
||||
} from '$lib/apis/images';
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
let loading = false;
|
||||
|
||||
let imageGenerationEngine = '';
|
||||
let enableImageGeneration = false;
|
||||
|
||||
let AUTOMATIC1111_BASE_URL = '';
|
||||
let OPENAI_API_KEY = '';
|
||||
|
||||
let selectedModel = '';
|
||||
let models = null;
|
||||
|
||||
let imageSize = '';
|
||||
let steps = 50;
|
||||
|
||||
const getModels = async () => {
|
||||
models = await getImageGenerationModels(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
selectedModel = await getDefaultImageGenerationModel(localStorage.token).catch((error) => {
|
||||
return '';
|
||||
});
|
||||
};
|
||||
|
||||
const updateAUTOMATIC1111UrlHandler = async () => {
|
||||
const res = await updateAUTOMATIC1111Url(localStorage.token, AUTOMATIC1111_BASE_URL).catch(
|
||||
(error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
}
|
||||
);
|
||||
|
||||
if (res) {
|
||||
AUTOMATIC1111_BASE_URL = res;
|
||||
|
||||
await getModels();
|
||||
|
||||
if (models) {
|
||||
toast.success($i18n.t('Server connection verified'));
|
||||
}
|
||||
} else {
|
||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
||||
}
|
||||
};
|
||||
const updateImageGeneration = async () => {
|
||||
const res = await updateImageGenerationConfig(
|
||||
localStorage.token,
|
||||
imageGenerationEngine,
|
||||
enableImageGeneration
|
||||
).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
imageGenerationEngine = res.engine;
|
||||
enableImageGeneration = res.enabled;
|
||||
}
|
||||
|
||||
if (enableImageGeneration) {
|
||||
config.set(await getBackendConfig(localStorage.token));
|
||||
getModels();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
const res = await getImageGenerationConfig(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (res) {
|
||||
imageGenerationEngine = res.engine;
|
||||
enableImageGeneration = res.enabled;
|
||||
}
|
||||
AUTOMATIC1111_BASE_URL = await getAUTOMATIC1111Url(localStorage.token);
|
||||
OPENAI_API_KEY = await getOpenAIKey(localStorage.token);
|
||||
|
||||
imageSize = await getImageSize(localStorage.token);
|
||||
steps = await getImageSteps(localStorage.token);
|
||||
|
||||
if (enableImageGeneration) {
|
||||
getModels();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
loading = true;
|
||||
|
||||
if (imageGenerationEngine === 'openai') {
|
||||
await updateOpenAIKey(localStorage.token, OPENAI_API_KEY);
|
||||
}
|
||||
|
||||
await updateDefaultImageGenerationModel(localStorage.token, selectedModel);
|
||||
|
||||
await updateImageSize(localStorage.token, imageSize).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
await updateImageSteps(localStorage.token, steps).catch((error) => {
|
||||
toast.error(error);
|
||||
return null;
|
||||
});
|
||||
|
||||
dispatch('save');
|
||||
loading = false;
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[24rem]">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('Image Settings')}</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Image Generation Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
|
||||
bind:value={imageGenerationEngine}
|
||||
placeholder={$i18n.t('Select a mode')}
|
||||
on:change={async () => {
|
||||
await updateImageGeneration();
|
||||
}}
|
||||
>
|
||||
<option value="">{$i18n.t('Default (Automatic1111)')}</option>
|
||||
<option value="openai">{$i18n.t('Open AI (Dall-E)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Image Generation (Experimental)')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
if (imageGenerationEngine === '' && AUTOMATIC1111_BASE_URL === '') {
|
||||
toast.error($i18n.t('AUTOMATIC1111 Base URL is required.'));
|
||||
enableImageGeneration = false;
|
||||
} else if (imageGenerationEngine === 'openai' && OPENAI_API_KEY === '') {
|
||||
toast.error($i18n.t('OpenAI API Key is required.'));
|
||||
enableImageGeneration = false;
|
||||
} else {
|
||||
enableImageGeneration = !enableImageGeneration;
|
||||
}
|
||||
|
||||
updateImageGeneration();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if enableImageGeneration === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
{#if imageGenerationEngine === ''}
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('AUTOMATIC1111 Base URL')}</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={$i18n.t('Enter URL (e.g. http://127.0.0.1:7860/)')}
|
||||
bind:value={AUTOMATIC1111_BASE_URL}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-700 rounded-lg transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
// updateOllamaAPIUrlHandler();
|
||||
|
||||
updateAUTOMATIC1111UrlHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<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 class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Include `--api` flag when running stable-diffusion-webui')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium"
|
||||
href="https://github.com/AUTOMATIC1111/stable-diffusion-webui/discussions/3734"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('(e.g. `sh webui.sh --api`)')}
|
||||
</a>
|
||||
</div>
|
||||
{:else if imageGenerationEngine === 'openai'}
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('OpenAI API Key')}</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={$i18n.t('Enter API Key')}
|
||||
bind:value={OPENAI_API_KEY}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if enableImageGeneration}
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Default 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={selectedModel}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
{#if !selectedModel}
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{/if}
|
||||
{#each models ?? [] as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Image Size')}</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={$i18n.t('Enter Image Size (e.g. 512x512)')}
|
||||
bind:value={imageSize}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Steps')}</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={$i18n.t('Enter Number of Steps (e.g. 50)')}
|
||||
bind:value={steps}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
|
||||
? ' cursor-not-allowed'
|
||||
: ''}"
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
>
|
||||
{$i18n.t('Save')}
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 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>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -2,10 +2,12 @@
|
||||
import { getBackendConfig } from '$lib/apis';
|
||||
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
|
||||
import { config, models, user } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import toast from 'svelte-french-toast';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveSettings: Function;
|
||||
|
||||
// Addons
|
||||
@@ -13,6 +15,7 @@
|
||||
let responseAutoCopy = false;
|
||||
let titleAutoGenerateModel = '';
|
||||
let fullScreenMode = false;
|
||||
let titleGenerationPrompt = '';
|
||||
|
||||
// Interface
|
||||
let promptSuggestions = [];
|
||||
@@ -56,8 +59,15 @@
|
||||
};
|
||||
|
||||
const updateInterfaceHandler = async () => {
|
||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||
await config.set(await getBackendConfig());
|
||||
if ($user.role === 'admin') {
|
||||
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
|
||||
await config.set(await getBackendConfig());
|
||||
}
|
||||
|
||||
saveSettings({
|
||||
titleAutoGenerateModel: titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined,
|
||||
titleGenerationPrompt: titleGenerationPrompt ? titleGenerationPrompt : undefined
|
||||
});
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
@@ -72,6 +82,11 @@
|
||||
showUsername = settings.showUsername ?? false;
|
||||
fullScreenMode = settings.fullScreenMode ?? false;
|
||||
titleAutoGenerateModel = settings.titleAutoGenerateModel ?? '';
|
||||
titleGenerationPrompt =
|
||||
settings.titleGenerationPrompt ??
|
||||
$i18n.t(
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
||||
) + ' {{prompt}}';
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -82,13 +97,13 @@
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll h-80">
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[22rem]">
|
||||
<div>
|
||||
<div class=" mb-1 text-sm font-medium">WebUI Add-ons</div>
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Title Auto-Generation</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -98,9 +113,9 @@
|
||||
type="button"
|
||||
>
|
||||
{#if titleAutoGenerate === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -108,7 +123,9 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Response AutoCopy to Clipboard</div>
|
||||
<div class=" self-center text-xs font-medium">
|
||||
{$i18n.t('Response AutoCopy to Clipboard')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -118,9 +135,9 @@
|
||||
type="button"
|
||||
>
|
||||
{#if responseAutoCopy === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -128,7 +145,7 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">Full Screen Mode</div>
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -138,9 +155,9 @@
|
||||
type="button"
|
||||
>
|
||||
{#if fullScreenMode === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -149,7 +166,7 @@
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">
|
||||
Display the username instead of "You" in the Chat
|
||||
{$i18n.t('Display the username instead of You in the Chat')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -160,9 +177,9 @@
|
||||
type="button"
|
||||
>
|
||||
{#if showUsername === true}
|
||||
<span class="ml-2 self-center">On</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">Off</span>
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
@@ -172,45 +189,33 @@
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium">Set Title Auto-Generation Model</div>
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Set Title Auto-Generation 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-800 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={titleAutoGenerateModel}
|
||||
placeholder="Select a model"
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
>
|
||||
<option value="" selected>Current Model</option>
|
||||
{#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
|
||||
>
|
||||
<option value="" selected>{$i18n.t('Current Model')}</option>
|
||||
{#each $models as model}
|
||||
{#if model.size != null}
|
||||
<option value={model.name} class="bg-gray-100 dark:bg-gray-700">
|
||||
{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}
|
||||
</option>
|
||||
{/if}
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
class="px-3 bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-800 dark:text-gray-100 rounded transition"
|
||||
on:click={() => {
|
||||
saveSettings({
|
||||
titleAutoGenerateModel:
|
||||
titleAutoGenerateModel !== '' ? titleAutoGenerateModel : undefined
|
||||
});
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 mr-2">
|
||||
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('Title Generation Prompt')}</div>
|
||||
<textarea
|
||||
bind:value={titleGenerationPrompt}
|
||||
class="w-full rounded-lg p-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none resize-none"
|
||||
rows="3"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -219,7 +224,9 @@
|
||||
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">Default Prompt Suggestions</div>
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Default Prompt Suggestions')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
@@ -292,19 +299,19 @@
|
||||
|
||||
{#if promptSuggestions.length > 0}
|
||||
<div class="text-xs text-left w-full mt-2">
|
||||
Adjusting these settings will apply changes universally to all users.
|
||||
{$i18n.t('Adjusting these settings will apply changes universally to all users.')}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<div class="flex justify-end text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded"
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
{$i18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,24 @@
|
||||
<script lang="ts">
|
||||
import toast from 'svelte-french-toast';
|
||||
import { getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { models, settings, user } from '$lib/stores';
|
||||
|
||||
import { getOllamaModels } from '$lib/apis/ollama';
|
||||
import { getOpenAIModels } from '$lib/apis/openai';
|
||||
import { getLiteLLMModels } from '$lib/apis/litellm';
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Account from './Settings/Account.svelte';
|
||||
import Advanced from './Settings/Advanced.svelte';
|
||||
import About from './Settings/About.svelte';
|
||||
import Models from './Settings/Models.svelte';
|
||||
import General from './Settings/General.svelte';
|
||||
import External from './Settings/External.svelte';
|
||||
import Interface from './Settings/Interface.svelte';
|
||||
import Audio from './Settings/Audio.svelte';
|
||||
import Chats from './Settings/Chats.svelte';
|
||||
import Connections from './Settings/Connections.svelte';
|
||||
import Images from './Settings/Images.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
|
||||
@@ -27,23 +31,29 @@
|
||||
|
||||
let selectedTab = 'general';
|
||||
|
||||
const getModels = async (type = 'all') => {
|
||||
const models = [];
|
||||
models.push(
|
||||
...(await getOllamaModels(localStorage.token).catch((error) => {
|
||||
toast.error(error);
|
||||
return [];
|
||||
}))
|
||||
);
|
||||
|
||||
if (type === 'all') {
|
||||
const openAIModels = await getOpenAIModels(localStorage.token).catch((error) => {
|
||||
const getModels = async () => {
|
||||
let models = await Promise.all([
|
||||
await getOllamaModels(localStorage.token).catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
});
|
||||
models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
||||
}
|
||||
}),
|
||||
await getOpenAIModels(localStorage.token).catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
}),
|
||||
await getLiteLLMModels(localStorage.token).catch((error) => {
|
||||
console.log(error);
|
||||
return null;
|
||||
})
|
||||
]);
|
||||
|
||||
models = models
|
||||
.filter((models) => models)
|
||||
.reduce((a, e, i, arr) => a.concat(e, ...(i < arr.length - 1 ? [{ name: 'hr' }] : [])), []);
|
||||
|
||||
// models.push(...(ollamaModels ? [{ name: 'hr' }, ...ollamaModels] : []));
|
||||
// models.push(...(openAIModels ? [{ name: 'hr' }, ...openAIModels] : []));
|
||||
// models.push(...(liteLLMModels ? [{ name: 'hr' }, ...liteLLMModels] : []));
|
||||
return models;
|
||||
};
|
||||
</script>
|
||||
@@ -51,7 +61,7 @@
|
||||
<Modal bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||
<div class=" text-lg font-medium self-center">Settings</div>
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Settings')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
@@ -99,34 +109,34 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">General</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'advanced'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'advanced';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Advanced</div>
|
||||
<div class=" self-center">{$i18n.t('General')}</div>
|
||||
</button>
|
||||
|
||||
{#if $user?.role === 'admin'}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'connections'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'connections';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Connections')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'models'
|
||||
@@ -150,31 +160,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Models</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'external'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'external';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M1 9.5A3.5 3.5 0 0 0 4.5 13H12a3 3 0 0 0 .917-5.857 2.503 2.503 0 0 0-3.198-3.019 3.5 3.5 0 0 0-6.628 2.171A3.5 3.5 0 0 0 1 9.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">External</div>
|
||||
<div class=" self-center">{$i18n.t('Models')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -196,12 +182,12 @@
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
d="M2 4.25A2.25 2.25 0 0 1 4.25 2h7.5A2.25 2.25 0 0 1 14 4.25v5.5A2.25 2.25 0 0 1 11.75 12h-1.312c.1.128.21.248.328.36a.75.75 0 0 1 .234.545v.345a.75.75 0 0 1-.75.75h-4.5a.75.75 0 0 1-.75-.75v-.345a.75.75 0 0 1 .234-.545c.118-.111.228-.232.328-.36H4.25A2.25 2.25 0 0 1 2 9.75v-5.5Zm2.25-.75a.75.75 0 0 0-.75.75v4.5c0 .414.336.75.75.75h7.5a.75.75 0 0 0 .75-.75v-4.5a.75.75 0 0 0-.75-.75h-7.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Interface</div>
|
||||
<div class=" self-center">{$i18n.t('Interface')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -228,9 +214,37 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Audio</div>
|
||||
<div class=" self-center">{$i18n.t('Audio')}</div>
|
||||
</button>
|
||||
|
||||
{#if $user.role === 'admin'}
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'images'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'images';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<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="M2 4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V4Zm10.5 5.707a.5.5 0 0 0-.146-.353l-1-1a.5.5 0 0 0-.708 0L9.354 9.646a.5.5 0 0 1-.708 0L6.354 7.354a.5.5 0 0 0-.708 0l-2 2a.5.5 0 0 0-.146.353V12a.5.5 0 0 0 .5.5h8a.5.5 0 0 0 .5-.5V9.707ZM12 5a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Images')}</div>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'chats'
|
||||
@@ -254,7 +268,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Chats</div>
|
||||
<div class=" self-center">{$i18n.t('Chats')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -280,7 +294,7 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">Account</div>
|
||||
<div class=" self-center">{$i18n.t('Account')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
@@ -306,46 +320,46 @@
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">About</div>
|
||||
<div class=" self-center">{$i18n.t('About')}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 md:min-h-[380px]">
|
||||
<div class="flex-1 md:min-h-[25rem]">
|
||||
{#if selectedTab === 'general'}
|
||||
<General
|
||||
{getModels}
|
||||
{saveSettings}
|
||||
on:save={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'advanced'}
|
||||
<Advanced
|
||||
on:save={() => {
|
||||
show = false;
|
||||
}}
|
||||
{saveSettings}
|
||||
/>
|
||||
{:else if selectedTab === 'models'}
|
||||
<Models {getModels} />
|
||||
{:else if selectedTab === 'external'}
|
||||
<External
|
||||
{:else if selectedTab === 'connections'}
|
||||
<Connections
|
||||
{getModels}
|
||||
on:save={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'interface'}
|
||||
<Interface
|
||||
{saveSettings}
|
||||
on:save={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'audio'}
|
||||
<Audio
|
||||
{saveSettings}
|
||||
on:save={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'images'}
|
||||
<Images
|
||||
{saveSettings}
|
||||
on:save={() => {
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'chats'}
|
||||
@@ -353,7 +367,7 @@
|
||||
{:else if selectedTab === 'account'}
|
||||
<Account
|
||||
saveHandler={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'about'}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import Modal from '../common/Modal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let downloadChat: Function;
|
||||
export let shareChat: Function;
|
||||
|
||||
@@ -17,11 +20,11 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
Share to OpenWebUI Community
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
</button>
|
||||
|
||||
<div class="flex justify-center space-x-1 mt-1.5">
|
||||
<div class=" self-center text-gray-400 text-xs font-medium">or</div>
|
||||
<div class=" self-center text-gray-400 text-xs font-medium">{$i18n.t('or')}</div>
|
||||
|
||||
<button
|
||||
class=" self-center rounded-full text-xs font-medium text-gray-700 dark:text-gray-500 underline"
|
||||
@@ -31,7 +34,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
Download as a File
|
||||
{$i18n.t('Download as a File')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import Modal from '../common/Modal.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
</script>
|
||||
|
||||
<Modal bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
|
||||
<div class=" text-lg font-medium self-center">Keyboard shortcuts</div>
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Keyboard shortcuts')}</div>
|
||||
<button
|
||||
class="self-center"
|
||||
on:click={() => {
|
||||
@@ -32,7 +35,7 @@
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<div class="flex flex-col space-y-3 w-full self-start">
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Open new chat</div>
|
||||
<div class=" text-sm">{$i18n.t('Open new chat')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
@@ -56,7 +59,7 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Focus chat input</div>
|
||||
<div class=" text-sm">{$i18n.t('Focus chat input')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
@@ -74,7 +77,7 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Copy last code block</div>
|
||||
<div class=" text-sm">{$i18n.t('Copy last code block')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
@@ -98,7 +101,7 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Copy last response</div>
|
||||
<div class=" text-sm">{$i18n.t('Copy last response')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
@@ -124,7 +127,7 @@
|
||||
|
||||
<div class="flex flex-col space-y-3 w-full self-start">
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Toggle settings</div>
|
||||
<div class=" text-sm">{$i18n.t('Toggle settings')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
@@ -141,7 +144,7 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Toggle sidebar</div>
|
||||
<div class=" text-sm">{$i18n.t('Toggle sidebar')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
@@ -165,7 +168,7 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Delete chat</div>
|
||||
<div class=" text-sm">{$i18n.t('Delete chat')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
@@ -188,7 +191,7 @@
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">Show shortcuts</div>
|
||||
<div class=" text-sm">{$i18n.t('Show shortcuts')}</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user