Unverified Commit 1eebb85f authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #3323 from open-webui/dev

0.3.6
parents 9e4dd4b8 b224ba00
...@@ -15,12 +15,13 @@ ...@@ -15,12 +15,13 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
import { config, models, settings } from '$lib/stores'; import { config, models, settings, user } from '$lib/stores';
import { synthesizeOpenAISpeech } from '$lib/apis/audio'; import { synthesizeOpenAISpeech } from '$lib/apis/audio';
import { imageGenerations } from '$lib/apis/images'; import { imageGenerations } from '$lib/apis/images';
import { import {
approximateToHumanReadable, approximateToHumanReadable,
extractSentences, extractSentences,
replaceTokens,
revertSanitizedResponseContent, revertSanitizedResponseContent,
sanitizeResponseContent sanitizeResponseContent
} from '$lib/utils'; } from '$lib/utils';
...@@ -74,7 +75,9 @@ ...@@ -74,7 +75,9 @@
let selectedCitation = null; let selectedCitation = null;
$: tokens = marked.lexer(sanitizeResponseContent(message?.content)); $: tokens = marked.lexer(
replaceTokens(sanitizeResponseContent(message?.content), model?.name, $user?.name)
);
const renderer = new marked.Renderer(); const renderer = new marked.Renderer();
...@@ -188,10 +191,6 @@ ...@@ -188,10 +191,6 @@
if (Object.keys(sentencesAudio).length - 1 === idx) { if (Object.keys(sentencesAudio).length - 1 === idx) {
speaking = null; speaking = null;
if ($settings.conversationMode) {
document.getElementById('voice-input-button')?.click();
}
} }
res(e); res(e);
...@@ -235,35 +234,40 @@ ...@@ -235,35 +234,40 @@
console.log(sentences); console.log(sentences);
sentencesAudio = sentences.reduce((a, e, i, arr) => { if (sentences.length > 0) {
a[i] = null; sentencesAudio = sentences.reduce((a, e, i, arr) => {
return a; a[i] = null;
}, {}); return a;
}, {});
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
let lastPlayedAudioPromise = Promise.resolve(); // Initialize a promise that resolves immediately
for (const [idx, sentence] of sentences.entries()) {
const res = await synthesizeOpenAISpeech( for (const [idx, sentence] of sentences.entries()) {
localStorage.token, const res = await synthesizeOpenAISpeech(
$settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice, localStorage.token,
sentence $settings?.audio?.tts?.voice ?? $config?.audio?.tts?.voice,
).catch((error) => { sentence
toast.error(error); ).catch((error) => {
toast.error(error);
speaking = null;
loadingSpeech = false; speaking = null;
loadingSpeech = false;
return null;
}); return null;
});
if (res) {
const blob = await res.blob(); if (res) {
const blobUrl = URL.createObjectURL(blob); const blob = await res.blob();
const audio = new Audio(blobUrl); const blobUrl = URL.createObjectURL(blob);
sentencesAudio[idx] = audio; const audio = new Audio(blobUrl);
loadingSpeech = false; sentencesAudio[idx] = audio;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx)); loadingSpeech = false;
lastPlayedAudioPromise = lastPlayedAudioPromise.then(() => playAudio(idx));
}
} }
} else {
speaking = null;
loadingSpeech = false;
} }
} else { } else {
let voices = []; let voices = [];
...@@ -302,7 +306,7 @@ ...@@ -302,7 +306,7 @@
}, 100); }, 100);
} }
} else { } else {
toast.error('No content to speak'); toast.error($i18n.t('No content to speak'));
} }
} }
}; };
...@@ -460,6 +464,18 @@ ...@@ -460,6 +464,18 @@
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-end space-x-1.5 text-sm font-medium"> <div class=" mt-2 mb-1 flex justify-end space-x-1.5 text-sm font-medium">
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import { user as _user } from '$lib/stores'; import { user as _user } from '$lib/stores';
import { getFileContentById } from '$lib/apis/files';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -97,6 +98,42 @@ ...@@ -97,6 +98,42 @@
<div class={$settings?.chatBubble ?? true ? 'self-end' : ''}> <div class={$settings?.chatBubble ?? true ? 'self-end' : ''}>
{#if file.type === 'image'} {#if file.type === 'image'}
<img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" /> <img src={file.url} alt="input" class=" max-h-96 rounded-lg" draggable="false" />
{:else if file.type === 'file'}
<button
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
type="button"
on:click={async () => {
if (file?.url) {
window.open(`${file?.url}/content`, '_blank').focus();
}
}}
>
<div class="p-2.5 bg-red-400 text-white rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
</div>
<div class="flex flex-col justify-center -space-y-0.5">
<div class=" dark:text-gray-100 text-sm font-medium line-clamp-1">
{file.name}
</div>
<div class=" text-gray-500 text-sm">{$i18n.t('File')}</div>
</div>
</button>
{:else if file.type === 'doc'} {:else if file.type === 'doc'}
<button <button
class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left" class="h-16 w-72 flex items-center space-x-3 px-2.5 dark:bg-gray-850 rounded-xl border border-gray-200 dark:border-none text-left"
......
...@@ -204,6 +204,7 @@ ...@@ -204,6 +204,7 @@
searchValue = ''; searchValue = '';
window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0); window.setTimeout(() => document.getElementById('model-search-input')?.focus(), 0);
}} }}
closeFocus={false}
> >
<DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}> <DropdownMenu.Trigger class="relative w-full" aria-label={placeholder}>
<div <div
......
...@@ -132,7 +132,8 @@ ...@@ -132,7 +132,8 @@
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
{#if !$WEBUI_NAME.includes('Open WebUI')} {#if !$WEBUI_NAME.includes('Open WebUI')}
<span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> - <span class=" text-gray-500 dark:text-gray-300 font-medium">{$WEBUI_NAME}</span> -
{/if}{$i18n.t('Created by')} {/if}
{$i18n.t('Created by')}
<a <a
class=" text-gray-500 dark:text-gray-300 font-medium" class=" text-gray-500 dark:text-gray-300 font-medium"
href="https://github.com/tjbck" href="https://github.com/tjbck"
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import Plus from '$lib/components/icons/Plus.svelte'; import Plus from '$lib/components/icons/Plus.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -21,11 +22,9 @@ ...@@ -21,11 +22,9 @@
let showAPIKeys = false; let showAPIKeys = false;
let showJWTToken = false;
let JWTTokenCopied = false; let JWTTokenCopied = false;
let APIKey = ''; let APIKey = '';
let showAPIKey = false;
let APIKeyCopied = false; let APIKeyCopied = false;
let profileImageInputElement: HTMLInputElement; let profileImageInputElement: HTMLInputElement;
...@@ -255,53 +254,7 @@ ...@@ -255,53 +254,7 @@
</div> </div>
<div class="flex mt-2"> <div class="flex mt-2">
<div class="flex w-full"> <SensitiveInput value={localStorage.token} readOnly={true} />
<input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
type={showJWTToken ? 'text' : 'password'}
value={localStorage.token}
disabled
/>
<button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
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 <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
...@@ -355,53 +308,7 @@ ...@@ -355,53 +308,7 @@
<div class="flex mt-2"> <div class="flex mt-2">
{#if APIKey} {#if APIKey}
<div class="flex w-full"> <SensitiveInput value={APIKey} readOnly={true} />
<input
class="w-full rounded-l-lg py-1.5 pl-4 text-sm bg-white dark:text-gray-300 dark:bg-gray-850 outline-none"
type={showAPIKey ? 'text' : 'password'}
value={APIKey}
disabled
/>
<button
class="px-2 transition rounded-r-lg bg-white dark:bg-gray-850"
on:click={() => {
showAPIKey = !showAPIKey;
}}
>
{#if showAPIKey}
<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 <button
class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg" class="ml-1.5 px-1.5 py-1 dark:hover:bg-gray-850 transition rounded-lg"
......
...@@ -32,7 +32,9 @@ ...@@ -32,7 +32,9 @@
saveSettings({ notificationEnabled: notificationEnabled }); saveSettings({ notificationEnabled: notificationEnabled });
} else { } else {
toast.error( toast.error(
'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.' $i18n.t(
'Response notifications cannot be activated as the website permissions have been denied. Please visit your browser settings to grant the necessary access.'
)
); );
} }
}; };
......
...@@ -13,6 +13,10 @@ ...@@ -13,6 +13,10 @@
export let saveSettings: Function; export let saveSettings: Function;
let backgroundImageUrl = null;
let inputFiles = null;
let filesInputElement;
// Addons // Addons
let titleAutoGenerate = true; let titleAutoGenerate = true;
let responseAutoCopy = false; let responseAutoCopy = false;
...@@ -28,6 +32,7 @@ ...@@ -28,6 +32,7 @@
let chatDirection: 'LTR' | 'RTL' = 'LTR'; let chatDirection: 'LTR' | 'RTL' = 'LTR';
let showEmojiInCall = false; let showEmojiInCall = false;
let voiceInterruption = false;
const toggleSplitLargeChunks = async () => { const toggleSplitLargeChunks = async () => {
splitLargeChunks = !splitLargeChunks; splitLargeChunks = !splitLargeChunks;
...@@ -54,6 +59,11 @@ ...@@ -54,6 +59,11 @@
saveSettings({ showEmojiInCall: showEmojiInCall }); saveSettings({ showEmojiInCall: showEmojiInCall });
}; };
const toggleVoiceInterruption = async () => {
voiceInterruption = !voiceInterruption;
saveSettings({ voiceInterruption: voiceInterruption });
};
const toggleUserLocation = async () => { const toggleUserLocation = async () => {
userLocation = !userLocation; userLocation = !userLocation;
...@@ -65,7 +75,7 @@ ...@@ -65,7 +75,7 @@
if (position) { if (position) {
await updateUserInfo(localStorage.token, { location: position }); await updateUserInfo(localStorage.token, { location: position });
toast.success('User location successfully retrieved.'); toast.success($i18n.t('User location successfully retrieved.'));
} else { } else {
userLocation = false; userLocation = false;
} }
...@@ -101,7 +111,9 @@ ...@@ -101,7 +111,9 @@
saveSettings({ responseAutoCopy: responseAutoCopy }); saveSettings({ responseAutoCopy: responseAutoCopy });
} else { } else {
toast.error( toast.error(
'Clipboard write permission denied. Please check your browser settings to grant the necessary access.' $i18n.t(
'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
)
); );
} }
}; };
...@@ -124,6 +136,7 @@ ...@@ -124,6 +136,7 @@
showUsername = $settings.showUsername ?? false; showUsername = $settings.showUsername ?? false;
showEmojiInCall = $settings.showEmojiInCall ?? false; showEmojiInCall = $settings.showEmojiInCall ?? false;
voiceInterruption = $settings.voiceInterruption ?? false;
chatBubble = $settings.chatBubble ?? true; chatBubble = $settings.chatBubble ?? true;
widescreenMode = $settings.widescreenMode ?? false; widescreenMode = $settings.widescreenMode ?? false;
...@@ -132,6 +145,8 @@ ...@@ -132,6 +145,8 @@
userLocation = $settings.userLocation ?? false; userLocation = $settings.userLocation ?? false;
defaultModelId = ($settings?.models ?? ['']).at(0); defaultModelId = ($settings?.models ?? ['']).at(0);
backgroundImageUrl = $settings.backgroundImageUrl ?? null;
}); });
</script> </script>
...@@ -142,13 +157,63 @@ ...@@ -142,13 +157,63 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem]"> <input
bind:this={filesInputElement}
bind:files={inputFiles}
type="file"
hidden
accept="image/*"
on:change={() => {
let reader = new FileReader();
reader.onload = (event) => {
let originalImageUrl = `${event.target.result}`;
backgroundImageUrl = originalImageUrl;
saveSettings({ backgroundImageUrl });
};
if (
inputFiles &&
inputFiles.length > 0 &&
['image/gif', 'image/webp', 'image/jpeg', 'image/png'].includes(inputFiles[0]['type'])
) {
reader.readAsDataURL(inputFiles[0]);
} else {
console.log(`Unsupported File Type '${inputFiles[0]['type']}'.`);
inputFiles = null;
}
}}
/>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[25rem] scrollbar-hidden">
<div class=" space-y-1 mb-3">
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-sm font-medium">{$i18n.t('Default Model')}</div>
</div>
</div>
<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={defaultModelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) as model}
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
{/each}
</select>
</div>
</div>
<hr class=" dark:border-gray-850" />
<div> <div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div> <div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
<div> <div>
<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('Chat Bubble UI')}</div> <div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -166,9 +231,33 @@ ...@@ -166,9 +231,33 @@
</div> </div>
</div> </div>
{#if !$settings.chatBubble}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('Display the username instead of You in the Chat')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleShowUsername();
}}
type="button"
>
{#if showUsername === 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>
{/if}
<div> <div>
<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('Widescreen Mode')}</div> <div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -188,7 +277,76 @@ ...@@ -188,7 +277,76 @@
<div> <div>
<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('Title Auto-Generation')}</div> <div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={toggleChangeChatDirection}
type="button"
>
{#if chatDirection === 'LTR'}
<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
{/if}
</button>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('Fluidly stream large external response chunks')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleSplitLargeChunks();
}}
type="button"
>
{#if splitLargeChunks === 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>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
{$i18n.t('Chat Background Image')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
if (backgroundImageUrl !== null) {
backgroundImageUrl = null;
saveSettings({ backgroundImageUrl });
} else {
filesInputElement.click();
}
}}
type="button"
>
{#if backgroundImageUrl !== null}
<span class="ml-2 self-center">{$i18n.t('Reset')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Upload')}</span>
{/if}
</button>
</div>
</div>
<div class=" my-1.5 text-sm font-medium">{$i18n.t('Chat')}</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -208,7 +366,7 @@ ...@@ -208,7 +366,7 @@
<div> <div>
<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"> <div class=" self-center text-xs">
{$i18n.t('Response AutoCopy to Clipboard')} {$i18n.t('Response AutoCopy to Clipboard')}
</div> </div>
...@@ -230,7 +388,7 @@ ...@@ -230,7 +388,7 @@
<div> <div>
<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('Allow User Location')}</div> <div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
...@@ -248,18 +406,20 @@ ...@@ -248,18 +406,20 @@
</div> </div>
</div> </div>
<div class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
<div> <div>
<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('Display Emoji in Call')}</div> <div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
on:click={() => { on:click={() => {
toggleEmojiInCall(); toggleVoiceInterruption();
}} }}
type="button" type="button"
> >
{#if showEmojiInCall === true} {#if voiceInterruption === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
...@@ -268,44 +428,18 @@ ...@@ -268,44 +428,18 @@
</div> </div>
</div> </div>
{#if !$settings.chatBubble}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Display the username instead of You in the Chat')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleShowUsername();
}}
type="button"
>
{#if showUsername === 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>
{/if}
<div> <div>
<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"> <div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
{$i18n.t('Fluidly stream large external response chunks')}
</div>
<button <button
class="p-1 px-3 text-xs flex rounded transition" class="p-1 px-3 text-xs flex rounded transition"
on:click={() => { on:click={() => {
toggleSplitLargeChunks(); toggleEmojiInCall();
}} }}
type="button" type="button"
> >
{#if splitLargeChunks === true} {#if showEmojiInCall === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span> <span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else} {:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span> <span class="ml-2 self-center">{$i18n.t('Off')}</span>
...@@ -314,47 +448,6 @@ ...@@ -314,47 +448,6 @@
</div> </div>
</div> </div>
</div> </div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={toggleChangeChatDirection}
type="button"
>
{#if chatDirection === 'LTR'}
<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
{/if}
</button>
</div>
</div>
<hr class=" dark:border-gray-850" />
<div class=" space-y-1 mb-3">
<div class="mb-2">
<div class="flex justify-between items-center text-xs">
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
</div>
</div>
<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={defaultModelId}
placeholder="Select a model"
>
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{#each $models.filter((model) => model.id) 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="flex justify-end text-sm font-medium"> <div class="flex justify-end text-sm font-medium">
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
dispatch('save'); dispatch('save');
}} }}
> >
<div class=" pr-1.5 overflow-y-scroll max-h-[25rem]"> <div class=" pr-1.5 py-1 overflow-y-scroll max-h-[25rem]">
<div> <div>
<div class="flex items-center justify-between mb-1"> <div class="flex items-center justify-between mb-1">
<Tooltip <Tooltip
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
</div> </div>
</Tooltip> </Tooltip>
<div class="mt-1"> <div class="">
<Switch <Switch
bind:state={enableMemory} bind:state={enableMemory}
on:change={async () => { on:change={async () => {
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
if (res) { if (res) {
console.log(res); console.log(res);
toast.success('Memory added successfully'); toast.success($i18n.t('Memory added successfully'));
content = ''; content = '';
show = false; show = false;
dispatch('save'); dispatch('save');
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
if (res) { if (res) {
console.log(res); console.log(res);
toast.success('Memory updated successfully'); toast.success($i18n.t('Memory updated successfully'));
dispatch('save'); dispatch('save');
show = false; show = false;
} }
......
...@@ -129,7 +129,7 @@ ...@@ -129,7 +129,7 @@
}); });
if (res) { if (res) {
toast.success('Memory deleted successfully'); toast.success($i18n.t('Memory deleted successfully'));
memories = await getMemories(localStorage.token); memories = await getMemories(localStorage.token);
} }
}} }}
...@@ -182,7 +182,7 @@ ...@@ -182,7 +182,7 @@
}); });
if (res) { if (res) {
toast.success('Memory cleared successfully'); toast.success($i18n.t('Memory cleared successfully'));
memories = []; memories = [];
} }
}}>{$i18n.t('Clear memory')}</button }}>{$i18n.t('Clear memory')}</button
......
<script lang="ts">
import { toast } from 'svelte-sonner';
import { config, functions, models, settings, tools, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import {
getUserValvesSpecById as getToolUserValvesSpecById,
getUserValvesById as getToolUserValvesById,
updateUserValvesById as updateToolUserValvesById
} from '$lib/apis/tools';
import {
getUserValvesSpecById as getFunctionUserValvesSpecById,
getUserValvesById as getFunctionUserValvesById,
updateUserValvesById as updateFunctionUserValvesById
} from '$lib/apis/functions';
import ManageModal from './Personalization/ManageModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
let tab = 'tools';
let selectedId = '';
let loading = false;
let valvesSpec = null;
let valves = {};
const getUserValves = async () => {
loading = true;
if (tab === 'tools') {
valves = await getToolUserValvesById(localStorage.token, selectedId);
valvesSpec = await getToolUserValvesSpecById(localStorage.token, selectedId);
} else if (tab === 'functions') {
valves = await getFunctionUserValvesById(localStorage.token, selectedId);
valvesSpec = await getFunctionUserValvesSpecById(localStorage.token, selectedId);
}
if (valvesSpec) {
// Convert array to string
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? []).join(',');
}
}
}
loading = false;
};
const submitHandler = async () => {
if (valvesSpec) {
// Convert string to array
for (const property in valvesSpec.properties) {
if (valvesSpec.properties[property]?.type === 'array') {
valves[property] = (valves[property] ?? '').split(',').map((v) => v.trim());
}
}
if (tab === 'tools') {
const res = await updateToolUserValvesById(localStorage.token, selectedId, valves).catch(
(error) => {
toast.error(error);
return null;
}
);
if (res) {
toast.success($i18n.t('Valves updated'));
valves = res;
}
} else if (tab === 'functions') {
const res = await updateFunctionUserValvesById(
localStorage.token,
selectedId,
valves
).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success($i18n.t('Valves updated'));
valves = res;
}
}
}
};
$: if (tab) {
selectedId = '';
}
$: if (selectedId) {
getUserValves();
}
</script>
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
submitHandler();
dispatch('save');
}}
>
<div class="flex flex-col pr-1.5 overflow-y-scroll max-h-[25rem]">
<div>
<div class="flex items-center justify-between mb-2">
<Tooltip content="">
<div class="text-sm font-medium">
{$i18n.t('Manage Valves')}
</div>
</Tooltip>
<div class=" self-end">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded text-xs bg-transparent outline-none text-right"
bind:value={tab}
placeholder="Select"
>
<option value="tools">{$i18n.t('Tools')}</option>
<option value="functions">{$i18n.t('Functions')}</option>
</select>
</div>
</div>
</div>
<div class="space-y-1">
<div class="flex gap-2">
<div class="flex-1">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={selectedId}
on:change={async () => {
await tick();
}}
>
{#if tab === 'tools'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
>{$i18n.t('Select a tool')}</option
>
{#each $tools as tool, toolIdx}
<option value={tool.id} class="bg-gray-100 dark:bg-gray-700">{tool.name}</option>
{/each}
{:else if tab === 'functions'}
<option value="" selected disabled class="bg-gray-100 dark:bg-gray-700"
>{$i18n.t('Select a function')}</option
>
{#each $functions as func, funcIdx}
<option value={func.id} class="bg-gray-100 dark:bg-700">{func.name}</option>
{/each}
{/if}
</select>
</div>
</div>
</div>
{#if selectedId}
<hr class="dark:border-gray-800 my-3 w-full" />
<div>
{#if !loading}
{#if valvesSpec}
{#each Object.keys(valvesSpec.properties) as property, idx}
<div class=" py-0.5 w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{valvesSpec.properties[property].title}
{#if (valvesSpec?.required ?? []).includes(property)}
<span class=" text-gray-500">*required</span>
{/if}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
type="button"
on:click={() => {
valves[property] = (valves[property] ?? null) === null ? '' : null;
}}
>
{#if (valves[property] ?? null) === null}
<span class="ml-2 self-center">
{#if (valvesSpec?.required ?? []).includes(property)}
{$i18n.t('None')}
{:else}
{$i18n.t('Default')}
{/if}
</span>
{:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div>
{#if (valves[property] ?? null) !== null}
<div class="flex mt-0.5 mb-1.5 space-x-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"
type="text"
placeholder={valvesSpec.properties[property].title}
bind:value={valves[property]}
autocomplete="off"
required
/>
</div>
</div>
{/if}
{#if (valvesSpec.properties[property]?.description ?? null) !== null}
<div class="text-xs text-gray-500">
{valvesSpec.properties[property].description}
</div>
{/if}
</div>
{/each}
{:else}
<div>No valves</div>
{/if}
{:else}
<Spinner className="size-5" />
{/if}
</div>
{/if}
</div>
<div class="flex justify-end 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>
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
import Personalization from './Settings/Personalization.svelte'; import Personalization from './Settings/Personalization.svelte';
import { updateUserSettings } from '$lib/apis/users'; import { updateUserSettings } from '$lib/apis/users';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import Valves from './Settings/Valves.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -65,8 +66,8 @@ ...@@ -65,8 +66,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'general' 'general'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'general'; selectedTab = 'general';
}} }}
...@@ -88,40 +89,11 @@ ...@@ -88,40 +89,11 @@
<div class=" self-center">{$i18n.t('General')}</div> <div class=" self-center">{$i18n.t('General')}</div>
</button> </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 ===
'admin'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={async () => {
await goto('/admin/settings');
show = false;
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
</button>
{/if}
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'interface' 'interface'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'interface'; selectedTab = 'interface';
}} }}
...@@ -146,8 +118,8 @@ ...@@ -146,8 +118,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'personalization' 'personalization'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'personalization'; selectedTab = 'personalization';
}} }}
...@@ -161,8 +133,8 @@ ...@@ -161,8 +133,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'audio' 'audio'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'audio'; selectedTab = 'audio';
}} }}
...@@ -185,11 +157,35 @@ ...@@ -185,11 +157,35 @@
<div class=" self-center">{$i18n.t('Audio')}</div> <div class=" self-center">{$i18n.t('Audio')}</div>
</button> </button>
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'valves'
? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => {
selectedTab = 'valves';
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
d="M18.75 12.75h1.5a.75.75 0 0 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM12 6a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 6ZM12 18a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 12 18ZM3.75 6.75h1.5a.75.75 0 1 0 0-1.5h-1.5a.75.75 0 0 0 0 1.5ZM5.25 18.75h-1.5a.75.75 0 0 1 0-1.5h1.5a.75.75 0 0 1 0 1.5ZM3 12a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 3 12ZM9 3.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5ZM12.75 12a2.25 2.25 0 1 1 4.5 0 2.25 2.25 0 0 1-4.5 0ZM9 15.75a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Z"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Valves')}</div>
</button>
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'chats' 'chats'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'chats'; selectedTab = 'chats';
}} }}
...@@ -214,8 +210,8 @@ ...@@ -214,8 +210,8 @@
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'account' 'account'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'account'; selectedTab = 'account';
}} }}
...@@ -237,11 +233,40 @@ ...@@ -237,11 +233,40 @@
<div class=" self-center">{$i18n.t('Account')}</div> <div class=" self-center">{$i18n.t('Account')}</div>
</button> </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 ===
'admin'
? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={async () => {
await goto('/admin/settings');
show = false;
}}
>
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
<path
fill-rule="evenodd"
d="M4.5 3.75a3 3 0 0 0-3 3v10.5a3 3 0 0 0 3 3h15a3 3 0 0 0 3-3V6.75a3 3 0 0 0-3-3h-15Zm4.125 3a2.25 2.25 0 1 0 0 4.5 2.25 2.25 0 0 0 0-4.5Zm-3.873 8.703a4.126 4.126 0 0 1 7.746 0 .75.75 0 0 1-.351.92 7.47 7.47 0 0 1-3.522.877 7.47 7.47 0 0 1-3.522-.877.75.75 0 0 1-.351-.92ZM15 8.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15ZM14.25 12a.75.75 0 0 1 .75-.75h3.75a.75.75 0 0 1 0 1.5H15a.75.75 0 0 1-.75-.75Zm.75 2.25a.75.75 0 0 0 0 1.5h3.75a.75.75 0 0 0 0-1.5H15Z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center">{$i18n.t('Admin Settings')}</div>
</button>
{/if}
<button <button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab === class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'about' 'about'
? 'bg-gray-200 dark:bg-gray-700' ? 'bg-gray-200 dark:bg-gray-800'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}" : ' hover:bg-gray-100 dark:hover:bg-gray-850'}"
on:click={() => { on:click={() => {
selectedTab = 'about'; selectedTab = 'about';
}} }}
...@@ -293,6 +318,13 @@ ...@@ -293,6 +318,13 @@
toast.success($i18n.t('Settings saved successfully!')); toast.success($i18n.t('Settings saved successfully!'));
}} }}
/> />
{:else if selectedTab === 'valves'}
<Valves
{saveSettings}
on:save={() => {
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'chats'} {:else if selectedTab === 'chats'}
<Chats {saveSettings} /> <Chats {saveSettings} />
{:else if selectedTab === 'account'} {:else if selectedTab === 'account'}
......
...@@ -10,11 +10,12 @@ ...@@ -10,11 +10,12 @@
import { python } from '@codemirror/lang-python'; import { python } from '@codemirror/lang-python';
import { oneDark } from '@codemirror/theme-one-dark'; import { oneDark } from '@codemirror/theme-one-dark';
import { onMount, createEventDispatcher } from 'svelte'; import { onMount, createEventDispatcher, getContext } from 'svelte';
import { formatPythonCode } from '$lib/apis/utils'; import { formatPythonCode } from '$lib/apis/utils';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let boilerplate = ''; export let boilerplate = '';
export let value = ''; export let value = '';
...@@ -37,7 +38,7 @@ ...@@ -37,7 +38,7 @@
changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }] changes: [{ from: 0, to: codeEditor.state.doc.length, insert: formattedCode }]
}); });
toast.success('Code formatted successfully'); toast.success($i18n.t('Code formatted successfully'));
return true; return true;
} }
return false; return false;
......
<script lang="ts"> <script lang="ts">
import { onMount, createEventDispatcher } from 'svelte'; import { onMount, getContext, createEventDispatcher } from 'svelte';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
const i18n = getContext('i18n');
import { flyAndScale } from '$lib/utils/transitions'; import { flyAndScale } from '$lib/utils/transitions';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let title = 'Confirm your action'; export let title = $i18n.t('Confirm your action');
export let message = 'This action cannot be undone. Do you wish to continue?'; export let message = $i18n.t('This action cannot be undone. Do you wish to continue?');
export let cancelLabel = 'Cancel'; export let cancelLabel = $i18n.t('Cancel');
export let confirmLabel = 'Confirm'; export let confirmLabel = $i18n.t('Confirm');
export let show = false; export let show = false;
let modalElement = null; let modalElement = null;
......
...@@ -23,24 +23,29 @@ ...@@ -23,24 +23,29 @@
}; };
const handleKeyDown = (event: KeyboardEvent) => { const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') { if (event.key === 'Escape' && isTopModal()) {
console.log('Escape'); console.log('Escape');
show = false; show = false;
} }
}; };
const isTopModal = () => {
const modals = document.getElementsByClassName('modal');
return modals.length && modals[modals.length - 1] === modalElement;
};
onMount(() => { onMount(() => {
mounted = true; mounted = true;
}); });
$: if (mounted) { $: if (show && modalElement) {
if (show) { document.body.appendChild(modalElement);
window.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
} else { } else if (modalElement) {
window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset'; document.body.removeChild(modalElement);
} document.body.style.overflow = 'unset';
} }
</script> </script>
...@@ -49,7 +54,7 @@ ...@@ -49,7 +54,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-[9999] overflow-hidden overscroll-contain" class="modal 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:mousedown={() => { on:mousedown={() => {
show = false; show = false;
......
<script lang="ts">
export let value: string = '';
export let placeholder = '';
export let readOnly = false;
export let outerClassName = 'flex flex-1';
export let inputClassName =
'w-full rounded-l-lg py-2 pl-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none';
export let showButtonClassName = 'px-2 transition rounded-r-lg bg-white dark:bg-gray-850';
let show = false;
</script>
<div class={outerClassName}>
<input
class={inputClassName}
{placeholder}
bind:value
required={!readOnly}
disabled={readOnly}
autocomplete="off"
{...{ type: show ? 'text' : 'password' }}
/>
<button
class={showButtonClassName}
on:click={(e) => {
e.preventDefault();
show = !show;
}}
>
{#if show}
<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>
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.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="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5c-3.162 0-6.133-.815-8.716-2.247m0 0A9.015 9.015 0 0 1 3 12c0-1.605.42-3.113 1.157-4.418"
/>
</svg>
<script lang="ts">
export let className = 'size-4';
export let strokeWidth = '1.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="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
/>
</svg>
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
let showShortcuts = false; let showShortcuts = false;
</script> </script>
<div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-10"> <div class=" hidden lg:flex fixed bottom-0 right-0 px-2 py-2 z-20">
<button <button
id="show-shortcuts-button" id="show-shortcuts-button"
class="hidden" class="hidden"
......
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