Unverified Commit 22c50f62 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #1631 from open-webui/dev

0.1.120
parents e0ebd7ae eefe0145
<script lang="ts"> <script lang="ts">
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let messageId = null;
export let show = false; export let show = false;
export let message; export let message;
const LIKE_REASONS = [ let LIKE_REASONS = [];
`Accurate information`, let DISLIKE_REASONS = [];
`Followed instructions perfectly`,
`Showcased creativity`, function loadReasons() {
`Positive attitude`, LIKE_REASONS = [
`Attention to detail`, $i18n.t('Accurate information'),
`Thorough explanation`, $i18n.t('Followed instructions perfectly'),
`Other` $i18n.t('Showcased creativity'),
$i18n.t('Positive attitude'),
$i18n.t('Attention to detail'),
$i18n.t('Thorough explanation'),
$i18n.t('Other')
]; ];
const DISLIKE_REASONS = [ DISLIKE_REASONS = [
`Don't like the style`, $i18n.t("Don't like the style"),
`Not factually correct`, $i18n.t('Not factually correct'),
`Didn't fully follow instructions`, $i18n.t("Didn't fully follow instructions"),
`Refused when it shouldn't have`, $i18n.t("Refused when it shouldn't have"),
`Being Lazy`, $i18n.t('Being lazy'),
`Other` $i18n.t('Other')
]; ];
}
let reasons = []; let reasons = [];
let selectedReason = null; let selectedReason = null;
...@@ -40,6 +48,7 @@ ...@@ -40,6 +48,7 @@
onMount(() => { onMount(() => {
selectedReason = message.annotation.reason; selectedReason = message.annotation.reason;
comment = message.annotation.comment; comment = message.annotation.comment;
loadReasons();
}); });
const submitHandler = () => { const submitHandler = () => {
...@@ -50,14 +59,17 @@ ...@@ -50,14 +59,17 @@
dispatch('submit'); dispatch('submit');
toast.success('Thanks for your feedback!'); toast.success($i18n.t('Thanks for your feedback!'));
show = false; show = false;
}; };
</script> </script>
<div class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"> <div
class=" my-2.5 rounded-xl px-4 py-3 border dark:border-gray-850"
id="message-feedback-{messageId}"
>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class=" text-sm">Tell us more:</div> <div class=" text-sm">{$i18n.t('Tell us more:')}</div>
<button <button
on:click={() => { on:click={() => {
...@@ -81,9 +93,9 @@ ...@@ -81,9 +93,9 @@
<div class="flex flex-wrap gap-2 text-sm mt-2.5"> <div class="flex flex-wrap gap-2 text-sm mt-2.5">
{#each reasons as reason} {#each reasons as reason}
<button <button
class="px-3.5 py-1 border dark:border-gray-850 dark:hover:bg-gray-850 {selectedReason === class="px-3.5 py-1 border dark:border-gray-850 hover:bg-gray-100 dark:hover:bg-gray-850 {selectedReason ===
reason reason
? 'dark:bg-gray-800' ? 'bg-gray-200 dark:bg-gray-800'
: ''} transition rounded-lg" : ''} transition rounded-lg"
on:click={() => { on:click={() => {
selectedReason = reason; selectedReason = reason;
...@@ -99,7 +111,7 @@ ...@@ -99,7 +111,7 @@
<textarea <textarea
bind:value={comment} bind:value={comment}
class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl" class="w-full text-sm px-1 py-2 bg-transparent outline-none resize-none rounded-xl"
placeholder="Feel free to add specific details" placeholder={$i18n.t('Feel free to add specific details')}
rows="2" rows="2"
/> />
</div> </div>
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { config, settings } from '$lib/stores'; import { config, settings } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/openai'; import { synthesizeOpenAISpeech } from '$lib/apis/audio';
import { imageGenerations } from '$lib/apis/images'; import { imageGenerations } from '$lib/apis/images';
import { import {
approximateToHumanReadable, approximateToHumanReadable,
...@@ -176,10 +176,12 @@ ...@@ -176,10 +176,12 @@
const toggleSpeakMessage = async () => { const toggleSpeakMessage = async () => {
if (speaking) { if (speaking) {
try {
speechSynthesis.cancel(); speechSynthesis.cancel();
sentencesAudio[speakingIdx].pause(); sentencesAudio[speakingIdx].pause();
sentencesAudio[speakingIdx].currentTime = 0; sentencesAudio[speakingIdx].currentTime = 0;
} catch {}
speaking = null; speaking = null;
speakingIdx = null; speakingIdx = null;
...@@ -221,6 +223,10 @@ ...@@ -221,6 +223,10 @@
sentence sentence
).catch((error) => { ).catch((error) => {
toast.error(error); toast.error(error);
speaking = null;
loadingSpeech = false;
return null; return null;
}); });
...@@ -230,7 +236,6 @@ ...@@ -230,7 +236,6 @@
const audio = new Audio(blobUrl); const audio = new Audio(blobUrl);
sentencesAudio[idx] = audio; sentencesAudio[idx] = audio;
loadingSpeech = false; loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
} }
} }
...@@ -551,6 +556,12 @@ ...@@ -551,6 +556,12 @@
on:click={() => { on:click={() => {
rateMessage(message.id, 1); rateMessage(message.id, 1);
showRateComment = true; showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}} }}
> >
<svg <svg
...@@ -580,6 +591,11 @@ ...@@ -580,6 +591,11 @@
on:click={() => { on:click={() => {
rateMessage(message.id, -1); rateMessage(message.id, -1);
showRateComment = true; showRateComment = true;
window.setTimeout(() => {
document
.getElementById(`message-feedback-${message.id}`)
?.scrollIntoView();
}, 0);
}} }}
> >
<svg <svg
...@@ -839,6 +855,7 @@ ...@@ -839,6 +855,7 @@
{#if showRateComment} {#if showRateComment}
<RateComment <RateComment
messageId={message.id}
bind:show={showRateComment} bind:show={showRateComment}
bind:message bind:message
on:submit={() => { on:submit={() => {
......
...@@ -176,10 +176,23 @@ ...@@ -176,10 +176,23 @@
e.target.style.height = ''; e.target.style.height = '';
e.target.style.height = `${e.target.scrollHeight}px`; e.target.style.height = `${e.target.scrollHeight}px`;
}} }}
on:keydown={(e) => {
if (e.key === 'Escape') {
document.getElementById('close-edit-message-button')?.click();
}
const isCmdOrCtrlPressed = e.metaKey || e.ctrlKey;
const isEnterPressed = e.key === 'Enter';
if (isCmdOrCtrlPressed && isEnterPressed) {
document.getElementById('save-edit-message-button')?.click();
}
}}
/> />
<div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium"> <div class=" mt-2 mb-1 flex justify-center space-x-2 text-sm font-medium">
<button <button
id="save-edit-message-button"
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg" class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-gray-100 transition rounded-lg"
on:click={() => { on:click={() => {
editMessageConfirmHandler(); editMessageConfirmHandler();
...@@ -189,6 +202,7 @@ ...@@ -189,6 +202,7 @@
</button> </button>
<button <button
id="close-edit-message-button"
class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg" class=" px-4 py-2 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-100 transition outline outline-1 outline-gray-200 dark:outline-gray-600 rounded-lg"
on:click={() => { on:click={() => {
cancelEditMessage(); cancelEditMessage();
......
<script lang="ts"> <script lang="ts">
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
import { user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext } from 'svelte'; import { createEventDispatcher, onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
...@@ -9,6 +11,9 @@ ...@@ -9,6 +11,9 @@
// Audio // Audio
let OpenAIUrl = '';
let OpenAIKey = '';
let STTEngines = ['', 'openai']; let STTEngines = ['', 'openai'];
let STTEngine = ''; let STTEngine = '';
...@@ -69,6 +74,18 @@ ...@@ -69,6 +74,18 @@
saveSettings({ speechAutoSend: speechAutoSend }); saveSettings({ speechAutoSend: speechAutoSend });
}; };
const updateConfigHandler = async () => {
const res = await updateAudioConfig(localStorage.token, {
url: OpenAIUrl,
key: OpenAIKey
});
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
}
};
onMount(async () => { onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}'); let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
...@@ -85,12 +102,24 @@ ...@@ -85,12 +102,24 @@
} else { } else {
getWebAPIVoices(); getWebAPIVoices();
} }
if ($user.role === 'admin') {
const res = await getAudioConfig(localStorage.token);
if (res) {
OpenAIUrl = res.OPENAI_API_BASE_URL;
OpenAIKey = res.OPENAI_API_KEY;
}
}
}); });
</script> </script>
<form <form
class="flex flex-col h-full justify-between space-y-3 text-sm" class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => { on:submit|preventDefault={async () => {
if ($user.role === 'admin') {
await updateConfigHandler();
}
saveSettings({ saveSettings({
audio: { audio: {
STTEngine: STTEngine !== '' ? STTEngine : undefined, STTEngine: STTEngine !== '' ? STTEngine : undefined,
...@@ -101,7 +130,7 @@ ...@@ -101,7 +130,7 @@
dispatch('save'); dispatch('save');
}} }}
> >
<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]">
<div> <div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div> <div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
...@@ -196,6 +225,26 @@ ...@@ -196,6 +225,26 @@
</div> </div>
</div> </div>
{#if $user.role === 'admin'}
{#if TTSEngine === 'openai'}
<div class="mt-1 flex gap-2 mb-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={OpenAIUrl}
required
/>
<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={OpenAIKey}
required
/>
</div>
{/if}
{/if}
<div class=" py-0.5 flex w-full justify-between"> <div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div> <div class=" self-center text-xs font-medium">{$i18n.t('Auto-playback response')}</div>
...@@ -223,7 +272,7 @@ ...@@ -223,7 +272,7 @@
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1"> <div class="flex-1">
<select <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={speaker} bind:value={speaker}
placeholder="Select a voice" placeholder="Select a voice"
> >
...@@ -241,16 +290,18 @@ ...@@ -241,16 +290,18 @@
<div class=" mb-2.5 text-sm font-medium">{$i18n.t('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 w-full">
<div class="flex-1"> <div class="flex-1">
<select <input
class="w-full rounded py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-800 outline-none" list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={speaker} bind:value={speaker}
placeholder="Select a voice" placeholder="Select a voice"
> />
<datalist id="voice-list">
{#each voices as voice} {#each voices as voice}
<option value={voice.name} class="bg-gray-100 dark:bg-gray-700">{voice.name}</option <option value={voice.name} />
>
{/each} {/each}
</select> </datalist>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -288,20 +288,20 @@ ...@@ -288,20 +288,20 @@
<div class="flex border-b dark:border-gray-600 w-full"> <div class="flex border-b dark:border-gray-600 w-full">
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600" class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Title (e.g. Tell me a fun fact)" placeholder={$i18n.t('Title (e.g. Tell me a fun fact)')}
bind:value={prompt.title[0]} bind:value={prompt.title[0]}
/> />
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600" class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Subtitle (e.g. about the Roman Empire)" placeholder={$i18n.t('Subtitle (e.g. about the Roman Empire)')}
bind:value={prompt.title[1]} bind:value={prompt.title[1]}
/> />
</div> </div>
<input <input
class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600" class="px-3 py-1.5 text-xs w-full bg-transparent outline-none border-r dark:border-gray-600"
placeholder="Prompt (e.g. Tell me a fun fact about the Roman Empire)" placeholder={$i18n.t('Prompt (e.g. Tell me a fun fact about the Roman Empire)')}
bind:value={prompt.content} bind:value={prompt.content}
/> />
</div> </div>
......
...@@ -3,9 +3,7 @@ ...@@ -3,9 +3,7 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { models, settings, user } from '$lib/stores'; import { models, settings, user } from '$lib/stores';
import { getOllamaModels } from '$lib/apis/ollama'; import { getModels } from '$lib/utils';
import { getOpenAIModels } from '$lib/apis/openai';
import { getLiteLLMModels } from '$lib/apis/litellm';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Account from './Settings/Account.svelte'; import Account from './Settings/Account.svelte';
...@@ -25,37 +23,11 @@ ...@@ -25,37 +23,11 @@
const saveSettings = async (updated) => { const saveSettings = async (updated) => {
console.log(updated); console.log(updated);
await settings.set({ ...$settings, ...updated }); await settings.set({ ...$settings, ...updated });
await models.set(await getModels()); await models.set(await getModels(localStorage.token));
localStorage.setItem('settings', JSON.stringify($settings)); localStorage.setItem('settings', JSON.stringify($settings));
}; };
let selectedTab = 'general'; let selectedTab = 'general';
const getModels = async () => {
let models = await Promise.all([
await getOllamaModels(localStorage.token).catch((error) => {
console.log(error);
return null;
}),
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> </script>
<Modal bind:show> <Modal bind:show>
......
...@@ -3,24 +3,27 @@ ...@@ -3,24 +3,27 @@
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats'; import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
import { chatId, modelfiles } from '$lib/stores'; import { modelfiles } from '$lib/stores';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import Modal from '../common/Modal.svelte'; import Modal from '../common/Modal.svelte';
import Link from '../icons/Link.svelte'; import Link from '../icons/Link.svelte';
export let chatId;
let chat = null; let chat = null;
let shareUrl = null;
const i18n = getContext('i18n'); const i18n = getContext('i18n');
const shareLocalChat = async () => { const shareLocalChat = async () => {
const _chat = chat; const _chat = chat;
const sharedChat = await shareChatById(localStorage.token, $chatId); const sharedChat = await shareChatById(localStorage.token, chatId);
const chatShareUrl = `${window.location.origin}/s/${sharedChat.id}`; shareUrl = `${window.location.origin}/s/${sharedChat.id}`;
console.log(shareUrl);
chat = await getChatById(localStorage.token, chatId);
toast.success($i18n.t('Copied shared chat URL to clipboard!')); return shareUrl;
copyToClipboard(chatShareUrl);
chat = await getChatById(localStorage.token, $chatId);
}; };
const shareChat = async () => { const shareChat = async () => {
...@@ -56,8 +59,8 @@ ...@@ -56,8 +59,8 @@
$: if (show) { $: if (show) {
(async () => { (async () => {
if ($chatId) { if (chatId) {
chat = await getChatById(localStorage.token, $chatId); chat = await getChatById(localStorage.token, chatId);
} else { } else {
chat = null; chat = null;
console.log(chat); console.log(chat);
...@@ -101,10 +104,10 @@ ...@@ -101,10 +104,10 @@
<button <button
class="underline" class="underline"
on:click={async () => { on:click={async () => {
const res = await deleteSharedChatById(localStorage.token, $chatId); const res = await deleteSharedChatById(localStorage.token, chatId);
if (res) { if (res) {
chat = await getChatById(localStorage.token, $chatId); chat = await getChatById(localStorage.token, chatId);
} }
}}>delete this link</button }}>delete this link</button
> and create a new shared link. > and create a new shared link.
...@@ -131,8 +134,12 @@ ...@@ -131,8 +134,12 @@
<button <button
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white" class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
type="button" type="button"
on:click={() => { on:pointerdown={() => {
shareLocalChat(); shareLocalChat();
}}
on:click={async () => {
copyToClipboard(shareUrl);
toast.success($i18n.t('Copied shared chat URL to clipboard!'));
show = false; show = false;
}} }}
> >
......
...@@ -15,8 +15,10 @@ ...@@ -15,8 +15,10 @@
return 'w-[16rem]'; return 'w-[16rem]';
} else if (size === 'sm') { } else if (size === 'sm') {
return 'w-[30rem]'; return 'w-[30rem]';
} else { } else if (size === 'md') {
return 'w-[44rem]'; return 'w-[44rem]';
} else {
return 'w-[48rem]';
} }
}; };
...@@ -47,7 +49,7 @@ ...@@ -47,7 +49,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions --> <!-- svelte-ignore a11y-no-static-element-interactions -->
<div <div
bind:this={modalElement} bind:this={modalElement}
class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-50 overflow-hidden overscroll-contain" class=" fixed top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center z-[9999] overflow-hidden overscroll-contain"
in:fade={{ duration: 10 }} in:fade={{ duration: 10 }}
on:click={() => { on:click={() => {
show = false; show = false;
......
...@@ -29,8 +29,8 @@ ...@@ -29,8 +29,8 @@
let embeddingEngine = ''; let embeddingEngine = '';
let embeddingModel = ''; let embeddingModel = '';
let openAIKey = ''; let OpenAIKey = '';
let openAIUrl = ''; let OpenAIUrl = '';
let chunkSize = 0; let chunkSize = 0;
let chunkOverlap = 0; let chunkOverlap = 0;
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
return; return;
} }
if ((embeddingEngine === 'openai' && openAIKey === '') || openAIUrl === '') { if ((embeddingEngine === 'openai' && OpenAIKey === '') || OpenAIUrl === '') {
toast.error($i18n.t('OpenAI URL/Key required.')); toast.error($i18n.t('OpenAI URL/Key required.'));
return; return;
} }
...@@ -93,8 +93,8 @@ ...@@ -93,8 +93,8 @@
...(embeddingEngine === 'openai' ...(embeddingEngine === 'openai'
? { ? {
openai_config: { openai_config: {
key: openAIKey, key: OpenAIKey,
url: openAIUrl url: OpenAIUrl
} }
} }
: {}) : {})
...@@ -133,8 +133,8 @@ ...@@ -133,8 +133,8 @@
embeddingEngine = embeddingConfig.embedding_engine; embeddingEngine = embeddingConfig.embedding_engine;
embeddingModel = embeddingConfig.embedding_model; embeddingModel = embeddingConfig.embedding_model;
openAIKey = embeddingConfig.openai_config.key; OpenAIKey = embeddingConfig.openai_config.key;
openAIUrl = embeddingConfig.openai_config.url; OpenAIUrl = embeddingConfig.openai_config.url;
} }
}; };
...@@ -192,14 +192,14 @@ ...@@ -192,14 +192,14 @@
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')} placeholder={$i18n.t('API Base URL')}
bind:value={openAIUrl} bind:value={OpenAIUrl}
required required
/> />
<input <input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none" class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')} placeholder={$i18n.t('API Key')}
bind:value={openAIKey} bind:value={OpenAIKey}
required required
/> />
</div> </div>
......
<script lang="ts">
export let className = 'size-3.5';
export let strokeWidth = '2.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z"
/>
</svg>
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
let showDownloadChatModal = false; let showDownloadChatModal = false;
</script> </script>
<ShareChatModal bind:show={showShareChatModal} /> <ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30"> <nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div <div
class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'} class=" flex {$settings?.fullScreenMode ?? null ? 'max-w-full' : 'max-w-3xl'}
......
...@@ -17,13 +17,17 @@ ...@@ -17,13 +17,17 @@
getChatById, getChatById,
getChatListByTagName, getChatListByTagName,
updateChatById, updateChatById,
getAllChatTags getAllChatTags,
archiveChatById
} from '$lib/apis/chats'; } from '$lib/apis/chats';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
import { fade, slide } from 'svelte/transition'; import { fade, slide } from 'svelte/transition';
import { WEBUI_BASE_URL } from '$lib/constants'; import { WEBUI_BASE_URL } from '$lib/constants';
import Tooltip from '../common/Tooltip.svelte'; import Tooltip from '../common/Tooltip.svelte';
import ChatMenu from './Sidebar/ChatMenu.svelte'; import ChatMenu from './Sidebar/ChatMenu.svelte';
import ShareChatModal from '../chat/ShareChatModal.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte';
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
let show = false; let show = false;
let navElement; let navElement;
...@@ -31,12 +35,16 @@ ...@@ -31,12 +35,16 @@
let title: string = 'UI'; let title: string = 'UI';
let search = ''; let search = '';
let shareChatId = null;
let selectedChatId = null; let selectedChatId = null;
let chatDeleteId = null; let chatDeleteId = null;
let chatTitleEditId = null; let chatTitleEditId = null;
let chatTitle = ''; let chatTitle = '';
let showArchivedChatsModal = false;
let showShareChatModal = false;
let showDropdown = false; let showDropdown = false;
let isEditing = false; let isEditing = false;
...@@ -134,8 +142,21 @@ ...@@ -134,8 +142,21 @@
localStorage.setItem('settings', JSON.stringify($settings)); localStorage.setItem('settings', JSON.stringify($settings));
location.href = '/'; location.href = '/';
}; };
const archiveChatHandler = async (id) => {
await archiveChatById(localStorage.token, id);
await chats.set(await getChatList(localStorage.token));
};
</script> </script>
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ArchivedChatsModal
bind:show={showArchivedChatsModal}
on:change={async () => {
await chats.set(await getChatList(localStorage.token));
}}
/>
<div <div
bind:this={navElement} bind:this={navElement}
class="h-screen max-h-[100dvh] min-h-screen {show class="h-screen max-h-[100dvh] min-h-screen {show
...@@ -544,9 +565,13 @@ ...@@ -544,9 +565,13 @@
</button> </button>
</div> </div>
{:else} {:else}
<div class="flex self-center space-x-1.5 z-10"> <div class="flex self-center space-x-1 z-10">
<ChatMenu <ChatMenu
chatId={chat.id} chatId={chat.id}
shareHandler={() => {
shareChatId = selectedChatId;
showShareChatModal = true;
}}
renameHandler={() => { renameHandler={() => {
chatTitle = chat.title; chatTitle = chat.title;
chatTitleEditId = chat.id; chatTitleEditId = chat.id;
...@@ -577,6 +602,18 @@ ...@@ -577,6 +602,18 @@
</svg> </svg>
</button> </button>
</ChatMenu> </ChatMenu>
<Tooltip content="Archive">
<button
aria-label="Archive"
class=" self-center dark:hover:text-white transition"
on:click={() => {
archiveChatHandler(chat.id);
}}
>
<ArchiveBox />
</button>
</Tooltip>
</div> </div>
{/if} {/if}
</div> </div>
...@@ -609,13 +646,13 @@ ...@@ -609,13 +646,13 @@
{#if showDropdown} {#if showDropdown}
<div <div
id="dropdownDots" id="dropdownDots"
class="absolute z-40 bottom-[70px] 4.5rem rounded-xl shadow w-[240px] bg-white dark:bg-gray-900" class="absolute z-40 bottom-[70px] rounded-lg shadow w-[240px] bg-white dark:bg-gray-900"
transition:fade|slide={{ duration: 100 }} transition:fade|slide={{ duration: 100 }}
> >
<div class="py-2 w-full"> <div class="p-1 py-2 w-full">
{#if $user.role === 'admin'} {#if $user.role === 'admin'}
<button <button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
goto('/admin'); goto('/admin');
showDropdown = false; showDropdown = false;
...@@ -641,7 +678,7 @@ ...@@ -641,7 +678,7 @@
</button> </button>
<button <button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
goto('/playground'); goto('/playground');
showDropdown = false; showDropdown = false;
...@@ -668,7 +705,20 @@ ...@@ -668,7 +705,20 @@
{/if} {/if}
<button <button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
showArchivedChatsModal = true;
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<ArchiveBox className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
</button>
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={async () => { on:click={async () => {
await showSettings.set(true); await showSettings.set(true);
showDropdown = false; showDropdown = false;
...@@ -699,11 +749,11 @@ ...@@ -699,11 +749,11 @@
</button> </button>
</div> </div>
<hr class=" dark:border-gray-700 m-0 p-0" /> <hr class=" dark:border-gray-800 m-0 p-0" />
<div class="py-2 w-full"> <div class="p-1 py-2 w-full">
<button <button
class="flex py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition" class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => { on:click={() => {
localStorage.removeItem('token'); localStorage.removeItem('token');
location.href = '/auth'; location.href = '/auth';
......
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Modal from '$lib/components/common/Modal.svelte';
import { archiveChatById, deleteChatById, getArchivedChatList } from '$lib/apis/chats';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const i18n = getContext('i18n');
export let show = false;
let chats = [];
const unarchiveChatHandler = async (chatId) => {
const res = await archiveChatById(localStorage.token, chatId).catch((error) => {
toast.error(error);
});
chats = await getArchivedChatList(localStorage.token);
dispatch('change');
};
const deleteChatHandler = async (chatId) => {
const res = await deleteChatById(localStorage.token, chatId).catch((error) => {
toast.error(error);
});
chats = await getArchivedChatList(localStorage.token);
};
$: if (show) {
(async () => {
chats = await getArchivedChatList(localStorage.token);
})();
}
</script>
<Modal size="lg" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 py-4">
<div class=" text-lg font-medium self-center">{$i18n.t('Archived Chats')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<hr class=" dark:border-gray-850" />
<div class="flex flex-col md:flex-row w-full px-5 py-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
{#if chats.length > 0}
<div class="text-left text-sm w-full mb-4">
<div class="relative overflow-x-auto">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 table-auto">
<thead
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 border-gray-800"
>
<tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2"> {$i18n.t('Created At')} </th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
<tbody>
{#each chats as chat, idx}
<tr
class="bg-white {idx !== chats.length - 1 &&
'border-b'} dark:bg-gray-900 dark:border-gray-850 text-xs"
>
<td class="px-3 py-1 w-2/3">
<a href="/c/{chat.id}" target="_blank">
<div class=" underline line-clamp-1">
{chat.title}
</div>
</a>
</td>
<td class=" px-3 py-1">
{dayjs(chat.created_at * 1000).format($i18n.t('MMMM DD, YYYY HH:mm'))}
</td>
<td class="px-3 py-1 text-right">
<div class="flex justify-end w-full">
<Tooltip content="Unarchive Chat">
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
unarchiveChatHandler(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 8.25H7.5a2.25 2.25 0 0 0-2.25 2.25v9a2.25 2.25 0 0 0 2.25 2.25h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25H15m0-3-3-3m0 0-3 3m3-3V15"
/>
</svg>
</button>
</Tooltip>
<Tooltip content="Delete Chat">
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
deleteChatHandler(chat.id);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
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>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
<!-- {#each chats as chat}
<div>
{JSON.stringify(chat)}
</div>
{/each} -->
</div>
{:else}
<div class="text-left text-sm w-full mb-8">You have no archived conversations.</div>
{/if}
</div>
</div>
</div>
</Modal>
...@@ -7,7 +7,9 @@ ...@@ -7,7 +7,9 @@
import Pencil from '$lib/components/icons/Pencil.svelte'; import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte'; import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
export let shareHandler: Function;
export let renameHandler: Function; export let renameHandler: Function;
export let deleteHandler: Function; export let deleteHandler: Function;
export let onClose: Function; export let onClose: Function;
...@@ -31,12 +33,22 @@ ...@@ -31,12 +33,22 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[150px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow" class="w-full max-w-[180px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow"
sideOffset={-2} sideOffset={-2}
side="bottom" side="bottom"
align="start" align="start"
transition={flyAndScale} transition={flyAndScale}
> >
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
shareHandler();
}}
>
<Share />
<div class="flex items-center">Share</div>
</DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => { on:click={() => {
......
{ {
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'с', 'м', 'ч', 'д', 'с' или '-1' за неограничен срок.", "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' или '-1' за неограничен срок.",
"(Beta)": "(Бета)", "(Beta)": "(Бета)",
"(e.g. `sh webui.sh --api`)": "(например `sh webui.sh --api`)", "(e.g. `sh webui.sh --api`)": "(например `sh webui.sh --api`)",
"(latest)": "(последна)", "(latest)": "(последна)",
......
{ {
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'', '', '時間', '', '' または '-1' で無期限。", "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' または '-1' で無期限。",
"(Beta)": "(ベータ版)", "(Beta)": "(ベータ版)",
"(e.g. sh webui.sh --api)": "(例: sh webui.sh --api)", "(e.g. sh webui.sh --api)": "(例: sh webui.sh --api)",
"(latest)": "(最新)", "(latest)": "(最新)",
......
This diff is collapsed.
{ {
"'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'', '', '시간', '', '' 또는 만료 없음 '-1'", "'s', 'm', 'h', 'd', 'w' or '-1' for no expiration.": "'s', 'm', 'h', 'd', 'w' 또는 만료 없음 '-1'",
"(Beta)": "(Beta)", "(Beta)": "(Beta)",
"(e.g. `sh webui.sh --api`)": "(예: `sh webui.sh --api`)", "(e.g. `sh webui.sh --api`)": "(예: `sh webui.sh --api`)",
"(latest)": "(latest)", "(latest)": "(latest)",
......
...@@ -43,6 +43,10 @@ ...@@ -43,6 +43,10 @@
"code": "ja-JP", "code": "ja-JP",
"title": "Japanese" "title": "Japanese"
}, },
{
"code": "ka-GE",
"title": "Georgian"
},
{ {
"code": "ko-KR", "code": "ko-KR",
"title": "Korean" "title": "Korean"
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment