Merge branch 'open-webui:main' into correct-markdown-rendering-model-descriptions
This commit is contained in:
@@ -335,7 +335,7 @@
|
||||
title="Content"
|
||||
srcdoc={contents[selectedContentIdx].content}
|
||||
class="w-full border-0 h-full rounded-none"
|
||||
sandbox="allow-scripts{($settings?.iframeSandboxAllowForms ?? false)
|
||||
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
|
||||
? ' allow-forms'
|
||||
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false)
|
||||
? ' allow-same-origin'
|
||||
|
||||
@@ -432,31 +432,20 @@
|
||||
}
|
||||
};
|
||||
|
||||
let pageSubscribe = null;
|
||||
onMount(async () => {
|
||||
loading = true;
|
||||
console.log('mounted');
|
||||
window.addEventListener('message', onMessageHandler);
|
||||
$socket?.on('chat-events', chatEventHandler);
|
||||
|
||||
page.subscribe((page) => {
|
||||
if (page.url.pathname === '/') {
|
||||
pageSubscribe = page.subscribe(async (p) => {
|
||||
if (p.url.pathname === '/') {
|
||||
await tick();
|
||||
initNewChat();
|
||||
}
|
||||
});
|
||||
|
||||
if (!$chatId) {
|
||||
chatIdUnsubscriber = chatId.subscribe(async (value) => {
|
||||
if (!value) {
|
||||
await tick(); // Wait for DOM updates
|
||||
await initNewChat();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if ($temporaryChatEnabled) {
|
||||
await goto('/');
|
||||
}
|
||||
}
|
||||
|
||||
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
prompt = '';
|
||||
files = [];
|
||||
@@ -515,6 +504,7 @@
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
pageSubscribe();
|
||||
chatIdUnsubscriber?.();
|
||||
window.removeEventListener('message', onMessageHandler);
|
||||
$socket?.off('chat-events', chatEventHandler);
|
||||
@@ -805,6 +795,11 @@
|
||||
`https://www.youtube.com/watch?v=${$page.url.searchParams.get('youtube')}`
|
||||
);
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.get('load-url')) {
|
||||
await uploadWeb($page.url.searchParams.get('load-url'));
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.get('web-search') === 'true') {
|
||||
webSearchEnabled = true;
|
||||
}
|
||||
@@ -813,6 +808,10 @@
|
||||
imageGenerationEnabled = true;
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.get('code-interpreter') === 'true') {
|
||||
codeInterpreterEnabled = true;
|
||||
}
|
||||
|
||||
if ($page.url.searchParams.get('tools')) {
|
||||
selectedToolIds = $page.url.searchParams
|
||||
.get('tools')
|
||||
@@ -859,6 +858,11 @@
|
||||
|
||||
const loadChat = async () => {
|
||||
chatId.set(chatIdProp);
|
||||
|
||||
if ($temporaryChatEnabled) {
|
||||
temporaryChatEnabled.set(false);
|
||||
}
|
||||
|
||||
chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
|
||||
await goto('/');
|
||||
return null;
|
||||
@@ -878,6 +882,11 @@
|
||||
(chatContent?.models ?? undefined) !== undefined
|
||||
? chatContent.models
|
||||
: [chatContent.models ?? ''];
|
||||
|
||||
if (!($user?.role === 'admin' || ($user?.permissions?.chat?.multiple_models ?? true))) {
|
||||
selectedModels = selectedModels.length > 0 ? [selectedModels[0]] : [''];
|
||||
}
|
||||
|
||||
oldSelectedModelIds = selectedModels;
|
||||
|
||||
history =
|
||||
@@ -1725,6 +1734,7 @@
|
||||
|
||||
history.messages[responseMessageId] = responseMessage;
|
||||
history.currentId = responseMessageId;
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
@@ -1821,7 +1831,8 @@
|
||||
childrenIds: [],
|
||||
role: 'user',
|
||||
content: userPrompt,
|
||||
models: selectedModels
|
||||
models: selectedModels,
|
||||
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
|
||||
};
|
||||
|
||||
if (parentId !== null) {
|
||||
@@ -2107,13 +2118,15 @@
|
||||
{stopResponse}
|
||||
{createMessagePair}
|
||||
onChange={(input) => {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(
|
||||
`chat-input${$chatId ? `-${$chatId}` : ''}`,
|
||||
JSON.stringify(input)
|
||||
);
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
|
||||
if (!$temporaryChatEnabled) {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(
|
||||
`chat-input${$chatId ? `-${$chatId}` : ''}`,
|
||||
JSON.stringify(input)
|
||||
);
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:upload={async (e) => {
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
{#if $user?.role === 'admin' || $user?.permissions.chat?.controls}
|
||||
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
|
||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||
|
||||
<Collapsible title={$i18n.t('System Prompt')} open={true} buttonClassName="w-full">
|
||||
@@ -80,7 +80,9 @@
|
||||
/>
|
||||
</div>
|
||||
</Collapsible>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || ($user?.permissions.chat?.controls ?? true)}
|
||||
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
|
||||
|
||||
<Collapsible title={$i18n.t('Advanced Params')} open={true} buttonClassName="w-full">
|
||||
|
||||
@@ -91,7 +91,15 @@
|
||||
|
||||
$: onChange({
|
||||
prompt,
|
||||
files: files.filter((file) => file.type !== 'image'),
|
||||
files: files
|
||||
.filter((file) => file.type !== 'image')
|
||||
.map((file) => {
|
||||
return {
|
||||
...file,
|
||||
user: undefined,
|
||||
access_control: undefined
|
||||
};
|
||||
}),
|
||||
selectedToolIds,
|
||||
selectedFilterIds,
|
||||
imageGenerationEnabled,
|
||||
@@ -299,6 +307,19 @@
|
||||
|
||||
const inputFilesHandler = async (inputFiles) => {
|
||||
console.log('Input files handler called with:', inputFiles);
|
||||
|
||||
if (
|
||||
($config?.file?.max_count ?? null) !== null &&
|
||||
files.length + inputFiles.length > $config?.file?.max_count
|
||||
) {
|
||||
toast.error(
|
||||
$i18n.t(`You can only chat with a maximum of {{maxCount}} file(s) at a time.`, {
|
||||
maxCount: $config?.file?.max_count
|
||||
})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
inputFiles.forEach((file) => {
|
||||
console.log('Processing file:', {
|
||||
name: file.name,
|
||||
@@ -334,9 +355,30 @@
|
||||
reader.onload = async (event) => {
|
||||
let imageUrl = event.target.result;
|
||||
|
||||
if ($settings?.imageCompression ?? false) {
|
||||
const width = $settings?.imageCompressionSize?.width ?? null;
|
||||
const height = $settings?.imageCompressionSize?.height ?? null;
|
||||
if (
|
||||
($settings?.imageCompression ?? false) ||
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
if ($settings?.imageCompression ?? false) {
|
||||
width = $settings?.imageCompressionSize?.width ?? null;
|
||||
height = $settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
if (width > ($config?.file?.image_compression?.width ?? null)) {
|
||||
width = $config?.file?.image_compression?.width ?? null;
|
||||
}
|
||||
if (height > ($config?.file?.image_compression?.height ?? null)) {
|
||||
height = $config?.file?.image_compression?.height ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (width || height) {
|
||||
imageUrl = await compressImage(imageUrl, width, height);
|
||||
|
||||
@@ -256,6 +256,10 @@
|
||||
};
|
||||
|
||||
const editMessage = async (messageId, { content, files }, submit = true) => {
|
||||
if ((selectedModels ?? []).filter((id) => id).length === 0) {
|
||||
toast.error($i18n.t('Model not selected'));
|
||||
return;
|
||||
}
|
||||
if (history.messages[messageId].role === 'user') {
|
||||
if (submit) {
|
||||
// New user message
|
||||
|
||||
@@ -188,9 +188,8 @@
|
||||
</div>
|
||||
<div slot="content">
|
||||
<div class="flex text-xs font-medium flex-wrap">
|
||||
{#each citations as citation, idx}
|
||||
{#each citations.slice(2) as citation, idx}
|
||||
<button
|
||||
id={`source-${id}-${idx + 1}`}
|
||||
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
|
||||
on:click={() => {
|
||||
showCitationModal = true;
|
||||
@@ -199,7 +198,7 @@
|
||||
>
|
||||
{#if citations.every((c) => c.distances !== undefined)}
|
||||
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
|
||||
{idx + 1}
|
||||
{idx + 3}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1 mx-1 truncate">
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
>
|
||||
{decodeString(document?.metadata?.name ?? document.source.name)}
|
||||
</a>
|
||||
{#if document?.metadata?.page}
|
||||
{#if Number.isInteger(document?.metadata?.page)}
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||
({$i18n.t('page')}
|
||||
{document.metadata.page + 1})
|
||||
|
||||
@@ -69,6 +69,21 @@
|
||||
>
|
||||
</iframe>
|
||||
{/if}
|
||||
{:else if token.text && token.text.includes('<iframe')}
|
||||
{@const match = token.text.match(/<iframe\s+[^>]*src="([^"]+)"[^>]*><\/iframe>/)}
|
||||
{@const iframeSrc = match && match[1]}
|
||||
{#if iframeSrc}
|
||||
<iframe
|
||||
class="w-full my-2"
|
||||
src={iframeSrc}
|
||||
title="Embedded content"
|
||||
frameborder="0"
|
||||
sandbox
|
||||
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
|
||||
></iframe>
|
||||
{:else}
|
||||
{token.text}
|
||||
{/if}
|
||||
{:else if token.text.includes(`<file type="html"`)}
|
||||
{@const match = token.text.match(/<file type="html" id="([^"]+)"/)}
|
||||
{@const fileId = match && match[1]}
|
||||
@@ -78,7 +93,7 @@
|
||||
src={`${WEBUI_BASE_URL}/api/v1/files/${fileId}/content/html`}
|
||||
title="Content"
|
||||
frameborder="0"
|
||||
sandbox="allow-scripts{($settings?.iframeSandboxAllowForms ?? false)
|
||||
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
|
||||
? ' allow-forms'
|
||||
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
|
||||
@@ -225,7 +225,7 @@
|
||||
<div
|
||||
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
|
||||
?.modelIdx == modelIdx
|
||||
? `border-gray-100 dark:border-gray-850 border-[1.5px] ${
|
||||
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
|
||||
$mobile ? 'min-w-full' : 'min-w-80'
|
||||
}`
|
||||
: `border-gray-100 dark:border-gray-850 border-dashed ${
|
||||
|
||||
@@ -605,7 +605,7 @@
|
||||
<ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
className={'size-8'}
|
||||
className={'size-8 assistant-message-profile-image'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@
|
||||
|
||||
<div class="flex flex-col text-left gap-1 mt-1.5">
|
||||
{#each followUps as followUp, idx (idx)}
|
||||
<button
|
||||
class=" mr-2 py-1.5 bg-transparent text-left text-sm flex items-center gap-2 px-1.5 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white transition"
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class=" mr-2 py-1.5 bg-transparent text-left text-sm flex items-center gap-2 px-1.5 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white transition cursor-pointer"
|
||||
on:click={() => onClick(followUp)}
|
||||
title={followUp}
|
||||
aria-label={followUp}
|
||||
@@ -26,7 +28,7 @@
|
||||
<div class="line-clamp-1">
|
||||
{followUp}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if idx < followUps.length - 1}
|
||||
<hr class="border-gray-100 dark:border-gray-850" />
|
||||
|
||||
@@ -107,7 +107,11 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
|
||||
<div
|
||||
class=" flex w-full user-message group"
|
||||
dir={$settings.chatDirection}
|
||||
id="message-{message.id}"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
|
||||
<ProfileImage
|
||||
@@ -115,7 +119,7 @@
|
||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
||||
'/user.png')
|
||||
: (user?.profile_image_url ?? '/user.png')}
|
||||
className={'size-8'}
|
||||
className={'size-8 user-message-profile-image'}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -143,6 +147,16 @@
|
||||
{/if}
|
||||
</Name>
|
||||
</div>
|
||||
{:else if message.timestamp}
|
||||
<div class="flex justify-end pb-1 pr-2">
|
||||
<div
|
||||
class="text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize translate-y-[1px]"
|
||||
>
|
||||
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
|
||||
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="chat-{message.role} w-full min-w-full markdown-prose">
|
||||
|
||||
@@ -425,7 +425,7 @@
|
||||
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
|
||||
bind:this={tagsContainerElement}
|
||||
>
|
||||
{#if (items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')) || items.find((item) => item.model?.direct) || tags.length > 0}
|
||||
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
|
||||
selectedConnectionType === ''
|
||||
@@ -440,7 +440,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')}
|
||||
{#if items.find((item) => item.model?.connection_type === 'local')}
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'local'
|
||||
? ''
|
||||
@@ -452,6 +452,9 @@
|
||||
>
|
||||
{$i18n.t('Local')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
{#if items.find((item) => item.model?.connection_type === 'external')}
|
||||
<button
|
||||
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'external'
|
||||
? ''
|
||||
|
||||
@@ -47,6 +47,15 @@
|
||||
|
||||
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
|
||||
|
||||
<button
|
||||
id="new-chat-button"
|
||||
class="hidden"
|
||||
on:click={() => {
|
||||
initNewChat();
|
||||
}}
|
||||
aria-label="New Chat"
|
||||
/>
|
||||
|
||||
<nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
|
||||
<div class="flex items-center w-full pl-1.5 pr-1">
|
||||
<div
|
||||
@@ -72,6 +81,24 @@
|
||||
<MenuLines />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if !$mobile}
|
||||
<Tooltip content={$i18n.t('New Chat')}>
|
||||
<button
|
||||
class=" flex {$showSidebar
|
||||
? 'md:hidden'
|
||||
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
initNewChat();
|
||||
}}
|
||||
aria-label="New Chat"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<PencilSquare className=" size-5" strokeWidth="2" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -135,23 +162,6 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={$i18n.t('New Chat')}>
|
||||
<button
|
||||
id="new-chat-button"
|
||||
class=" flex {$showSidebar
|
||||
? 'md:hidden'
|
||||
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
initNewChat();
|
||||
}}
|
||||
aria-label="New Chat"
|
||||
>
|
||||
<div class=" m-auto self-center">
|
||||
<PencilSquare className=" size-5" strokeWidth="2" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if $user !== undefined && $user !== null}
|
||||
<UserMenu
|
||||
className="max-w-[240px]"
|
||||
@@ -183,6 +193,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $temporaryChatEnabled && $chatId === 'local'}
|
||||
<div class=" w-full z-30 text-center">
|
||||
<div class="text-xs text-gray-500">{$i18n.t('Temporary Chat')}</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
|
||||
<div class=" w-full z-30 mt-5">
|
||||
<div class=" flex flex-col gap-1 w-full">
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
|
||||
{#if $temporaryChatEnabled}
|
||||
<Tooltip
|
||||
content={$i18n.t('This chat won’t appear in history and your messages will not be saved.')}
|
||||
content={$i18n.t("This chat won't appear in history and your messages will not be saved.")}
|
||||
className="w-full flex justify-center mb-0.5"
|
||||
placement="top"
|
||||
>
|
||||
@@ -107,7 +107,7 @@
|
||||
class="w-full text-3xl text-gray-800 dark:text-gray-100 text-center flex items-center gap-4 font-primary"
|
||||
>
|
||||
<div class="w-full flex flex-col justify-center items-center">
|
||||
<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5">
|
||||
<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5 max-w-xl">
|
||||
<div class="flex shrink-0 justify-center">
|
||||
<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
|
||||
{#each models as model, modelIdx}
|
||||
@@ -138,9 +138,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" text-3xl @sm:text-3xl line-clamp-1" in:fade={{ duration: 100 }}>
|
||||
<div
|
||||
class=" text-3xl @sm:text-3xl line-clamp-1 flex items-center"
|
||||
in:fade={{ duration: 100 }}
|
||||
>
|
||||
{#if models[selectedModelIdx]?.name}
|
||||
{models[selectedModelIdx]?.name}
|
||||
<Tooltip
|
||||
content={models[selectedModelIdx]?.name}
|
||||
placement="top"
|
||||
className=" flex items-center "
|
||||
>
|
||||
<span class="line-clamp-1">
|
||||
{models[selectedModelIdx]?.name}
|
||||
</span>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
{$i18n.t('Hello, {{name}}', { name: $user?.name })}
|
||||
{/if}
|
||||
@@ -205,10 +216,12 @@
|
||||
{createMessagePair}
|
||||
placeholder={$i18n.t('How can I help you today?')}
|
||||
onChange={(input) => {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(`chat-input`, JSON.stringify(input));
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input`);
|
||||
if (!$temporaryChatEnabled) {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(`chat-input`, JSON.stringify(input));
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
on:upload={(e) => {
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
|
||||
<div id="tab-about" class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
|
||||
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div id="tab-account" class="flex flex-col h-full justify-between text-sm">
|
||||
<div class=" overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<input
|
||||
id="profile-image-input"
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
<div>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.'
|
||||
"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model's built-in tool-calling capabilities, but requires the model to inherently support this feature."
|
||||
)}
|
||||
placement="top-start"
|
||||
className="inline-tooltip"
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
</script>
|
||||
|
||||
<form
|
||||
id="tab-audio"
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
saveSettings({
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
<ArchivedChatsModal bind:show={showArchivedChatsModal} onUpdate={handleArchivedChatsChange} />
|
||||
|
||||
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
|
||||
<div id="tab-chats" class="flex flex-col h-full justify-between space-y-3 text-sm">
|
||||
<div class=" space-y-2 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div class="flex flex-col">
|
||||
<input
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
<AddConnectionModal direct bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
|
||||
|
||||
<form
|
||||
id="tab-connections"
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateHandler();
|
||||
@@ -126,7 +127,11 @@
|
||||
</div>
|
||||
|
||||
<div class="my-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
<div
|
||||
class="text-xs {($settings?.highContrastMode ?? false)
|
||||
? 'text-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-500'}"
|
||||
>
|
||||
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
|
||||
<br />
|
||||
{$i18n.t(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { getContext, tick } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
@@ -65,7 +66,7 @@
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
|
||||
class={`w-full bg-transparent ${($settings?.highContrastMode ?? false) ? '' : 'outline-hidden'} ${pipeline ? 'pr-8' : ''}`}
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={url}
|
||||
autocomplete="off"
|
||||
@@ -73,7 +74,7 @@
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
inputClassName=" outline-hidden bg-transparent w-full"
|
||||
inputClassName="bg-transparent w-full"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={key}
|
||||
/>
|
||||
@@ -83,6 +84,7 @@
|
||||
<div class="flex gap-1">
|
||||
<Tooltip content={$i18n.t('Configure')} className="self-start">
|
||||
<button
|
||||
aria-label={$i18n.t('Open modal to configure connection')}
|
||||
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
on:click={() => {
|
||||
showConfigModal = true;
|
||||
|
||||
@@ -187,7 +187,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full justify-between text-sm">
|
||||
<div class="flex flex-col h-full justify-between text-sm" id="tab-general">
|
||||
<div class=" overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div class="">
|
||||
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
|
||||
@@ -196,7 +196,9 @@
|
||||
<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-sm py-2 px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent text-right {$settings.highContrastMode
|
||||
? ''
|
||||
: 'outline-hidden'}"
|
||||
bind:value={selectedTheme}
|
||||
placeholder="Select a theme"
|
||||
on:change={() => themeChangeHandler(selectedTheme)}
|
||||
@@ -216,7 +218,9 @@
|
||||
<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-sm py-2 px-2 text-xs bg-transparent outline-hidden text-right"
|
||||
class="dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent text-right {$settings.highContrastMode
|
||||
? ''
|
||||
: 'outline-hidden'}"
|
||||
bind:value={lang}
|
||||
placeholder="Select a language"
|
||||
on:change={(e) => {
|
||||
@@ -263,7 +267,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $user?.role === 'admin' || $user?.permissions.chat?.controls}
|
||||
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
|
||||
<hr class="border-gray-50 dark:border-gray-850 my-3" />
|
||||
|
||||
<div>
|
||||
@@ -275,7 +279,9 @@
|
||||
placeholder={$i18n.t('Enter system prompt here')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || ($user?.permissions.chat?.controls ?? true)}
|
||||
<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>
|
||||
|
||||
@@ -348,6 +348,7 @@
|
||||
</script>
|
||||
|
||||
<form
|
||||
id="tab-interface"
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateInterfaceHandler();
|
||||
@@ -384,15 +385,16 @@
|
||||
|
||||
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
||||
<div>
|
||||
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
|
||||
<h1 class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</h1>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="high-contrast-mode-label" class=" self-center text-xs">
|
||||
{$i18n.t('High Contrast Mode')} ({$i18n.t('Beta')})
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="high-contrast-mode-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleHighContrastMode();
|
||||
@@ -410,9 +412,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div>
|
||||
<div id="landing-page-mode-label" class=" self-center text-xs">
|
||||
{$i18n.t('Landing Page Mode')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="landing-page-mode-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleLandingPageMode();
|
||||
@@ -430,9 +435,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
|
||||
<div id="chat-bubble-ui-label" class=" self-center text-xs">
|
||||
{$i18n.t('Chat Bubble UI')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="chat-bubble-ui-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleChatBubble();
|
||||
@@ -451,11 +459,12 @@
|
||||
{#if !$settings.chatBubble}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="chat-bubble-username-label" class=" self-center text-xs">
|
||||
{$i18n.t('Display the username instead of You in the Chat')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="chat-bubble-username-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleShowUsername();
|
||||
@@ -474,13 +483,16 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
|
||||
<div id="widescreen-mode-label" class=" self-center text-xs">
|
||||
{$i18n.t('Widescreen Mode')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleWidescreenMode();
|
||||
}}
|
||||
aria-labelledby="widescreen-mode-label"
|
||||
type="button"
|
||||
>
|
||||
{#if widescreenMode === true}
|
||||
@@ -494,9 +506,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>
|
||||
<div id="chat-direction-label" class=" self-center text-xs">
|
||||
{$i18n.t('Chat direction')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="chat-direction-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={toggleChangeChatDirection}
|
||||
type="button"
|
||||
@@ -513,12 +528,13 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div id="notification-sound-label" class=" self-center text-xs">
|
||||
{$i18n.t('Notification Sound')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="notification-sound-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleNotificationSound();
|
||||
@@ -537,11 +553,12 @@
|
||||
{#if notificationSound}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="play-notification-sound-label" class=" self-center text-xs">
|
||||
{$i18n.t('Always Play Notification Sound')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="play-notification-sound-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleNotificationSoundAlways();
|
||||
@@ -561,11 +578,12 @@
|
||||
{#if $user?.role === 'admin'}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="toast-notifications-label" class=" self-center text-xs">
|
||||
{$i18n.t('Toast notifications for new updates')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="toast-notifications-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleShowUpdateToast();
|
||||
@@ -583,11 +601,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="whats-new-label" class=" self-center text-xs">
|
||||
{$i18n.t(`Show "What's New" modal on login`)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="whats-new-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleShowChangelog();
|
||||
@@ -608,9 +627,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
|
||||
<div id="auto-generation-label" class=" self-center text-xs">
|
||||
{$i18n.t('Title Auto-Generation')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="auto-generation-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleTitleAutoGenerate();
|
||||
@@ -631,6 +653,7 @@
|
||||
<div class=" self-center text-xs">{$i18n.t('Follow-Up Auto-Generation')}</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="auto-generation-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleAutoFollowUps();
|
||||
@@ -648,9 +671,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div>
|
||||
<div id="chat-tags-label" class=" self-center text-xs">
|
||||
{$i18n.t('Chat Tags Auto-Generation')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="chat-tags-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleAutoTags();
|
||||
@@ -668,11 +694,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="detect-artifacts-label" class=" self-center text-xs">
|
||||
{$i18n.t('Detect Artifacts Automatically')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="detect-artifacts-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleDetectArtifacts();
|
||||
@@ -690,11 +717,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="auto-copy-label" class=" self-center text-xs">
|
||||
{$i18n.t('Auto-Copy Response to Clipboard')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="auto-copy-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleResponseAutoCopy();
|
||||
@@ -712,11 +740,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="rich-input-label" class=" self-center text-xs">
|
||||
{$i18n.t('Rich Text Input for Chat')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="rich-input-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleRichTextInput();
|
||||
@@ -735,11 +764,12 @@
|
||||
{#if $config?.features?.enable_autocomplete_generation && richTextInput}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="prompt-autocompletion-label" class=" self-center text-xs">
|
||||
{$i18n.t('Prompt Autocompletion')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="prompt-autocompletion-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
togglePromptAutocomplete();
|
||||
@@ -758,11 +788,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="paste-large-label" class=" self-center text-xs">
|
||||
{$i18n.t('Paste Large Text as File')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="paste-large-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleLargeTextAsFile();
|
||||
@@ -780,11 +811,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="copy-formatted-label" class=" self-center text-xs">
|
||||
{$i18n.t('Copy Formatted Text')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="copy-formatted-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleCopyFormatted();
|
||||
@@ -802,9 +834,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Always Collapse Code Blocks')}</div>
|
||||
<div id="always-collapse-label" class=" self-center text-xs">
|
||||
{$i18n.t('Always Collapse Code Blocks')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="always-collapse-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleCollapseCodeBlocks();
|
||||
@@ -822,9 +857,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Always Expand Details')}</div>
|
||||
<div id="always-expand-label" class=" self-center text-xs">
|
||||
{$i18n.t('Always Expand Details')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="always-expand-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleExpandDetails();
|
||||
@@ -842,11 +880,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="chat-background-label" class=" self-center text-xs">
|
||||
{$i18n.t('Chat Background Image')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="chat-background-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
if (backgroundImageUrl !== null) {
|
||||
@@ -868,10 +907,11 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div id="allow-user-location-label" class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="allow-user-location-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleUserLocation();
|
||||
@@ -889,11 +929,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="haptic-feedback-label" class=" self-center text-xs">
|
||||
{$i18n.t('Haptic Feedback')} ({$i18n.t('Android')})
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="haptic-feedback-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleHapticFeedback();
|
||||
@@ -911,11 +952,12 @@
|
||||
|
||||
<!-- <div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="fluidly-stream-label" class=" self-center text-xs">
|
||||
{$i18n.t('Fluidly stream large external response chunks')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="fluidly-stream-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleSplitLargeChunks();
|
||||
@@ -933,11 +975,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="enter-key-behavior-label" class=" self-center text-xs">
|
||||
{$i18n.t('Enter Key Behavior')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="enter-key-behavior-label"
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
togglectrlEnterToSend();
|
||||
@@ -955,11 +998,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="scroll-on-branch-change-label" class=" self-center text-xs">
|
||||
{$i18n.t('Scroll On Branch Change')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="scroll-on-branch-change-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
togglesScrollOnBranchChange();
|
||||
@@ -977,9 +1021,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Web Search in Chat')}</div>
|
||||
<div id="web-search-in-chat-label" class=" self-center text-xs">
|
||||
{$i18n.t('Web Search in Chat')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="web-search-in-chat-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleWebSearch();
|
||||
@@ -997,9 +1044,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('iframe Sandbox Allow Same Origin')}</div>
|
||||
<div id="iframe-sandbox-allow-same-origin-label" class=" self-center text-xs">
|
||||
{$i18n.t('iframe Sandbox Allow Same Origin')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="iframe-sandbox-allow-same-origin-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleIframeSandboxAllowSameOrigin();
|
||||
@@ -1017,9 +1067,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('iframe Sandbox Allow Forms')}</div>
|
||||
<div id="iframe-sandbox-allow-forms-label" class=" self-center text-xs">
|
||||
{$i18n.t('iframe Sandbox Allow Forms')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="iframe-sandbox-allow-forms-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleIframeSandboxAllowForms();
|
||||
@@ -1037,11 +1090,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">
|
||||
<div id="stylized-pdf-export-label" class=" self-center text-xs">
|
||||
{$i18n.t('Stylized PDF Export')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="stylized-pdf-export-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleStylizedPdfExport();
|
||||
@@ -1064,6 +1118,7 @@
|
||||
<div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="allow-voice-interruption-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleVoiceInterruption();
|
||||
@@ -1081,9 +1136,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
|
||||
<div id="display-emoji-label" class=" self-center text-xs">
|
||||
{$i18n.t('Display Emoji in Call')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="display-emoji-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleEmojiInCall();
|
||||
@@ -1103,9 +1161,12 @@
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs">{$i18n.t('Image Compression')}</div>
|
||||
<div id="image-compression-label" class=" self-center text-xs">
|
||||
{$i18n.t('Image Compression')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
aria-labelledby="image-compression-label"
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleImageCompression();
|
||||
@@ -1124,9 +1185,14 @@
|
||||
{#if imageCompression}
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between text-xs">
|
||||
<div class=" self-center text-xs">{$i18n.t('Image Max Compression Size')}</div>
|
||||
<div id="image-compression-size-label" class=" self-center text-xs">
|
||||
{$i18n.t('Image Max Compression Size')}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="sr-only" for="image-comp-width"
|
||||
>{$i18n.t('Image Max Compression Size width')}</label
|
||||
>
|
||||
<input
|
||||
bind:value={imageCompressionSize.width}
|
||||
type="number"
|
||||
@@ -1134,6 +1200,9 @@
|
||||
min="0"
|
||||
placeholder="Width"
|
||||
/>x
|
||||
<label class="sr-only" for="image-comp-height"
|
||||
>{$i18n.t('Image Max Compression Size height')}</label
|
||||
>
|
||||
<input
|
||||
bind:value={imageCompressionSize.height}
|
||||
type="number"
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<ManageModal bind:show={showManageModal} />
|
||||
|
||||
<form
|
||||
id="tab-personalization"
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
dispatch('save');
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<AddServerModal bind:show={showConnectionModal} onSubmit={addConnectionHandler} direct />
|
||||
|
||||
<form
|
||||
id="tab-tools"
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
updateHandler();
|
||||
@@ -60,6 +61,7 @@
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
aria-label={$i18n.t(`Add Connection`)}
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showConnectionModal = true;
|
||||
@@ -89,7 +91,10 @@
|
||||
</div>
|
||||
|
||||
<div class="my-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
<div
|
||||
class={`text-xs
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||
<br />
|
||||
{$i18n.t(
|
||||
|
||||
@@ -211,7 +211,9 @@
|
||||
: []),
|
||||
|
||||
...($user?.role === 'admin' ||
|
||||
($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)
|
||||
($user?.role === 'user' &&
|
||||
$user?.permissions?.features?.direct_tool_servers &&
|
||||
$config?.features?.direct_tool_servers)
|
||||
? [
|
||||
{
|
||||
id: 'tools',
|
||||
@@ -554,6 +556,7 @@
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pt-1 pb-4 md:space-x-4">
|
||||
<div
|
||||
role="tablist"
|
||||
id="settings-tabs-container"
|
||||
class="tabs flex flex-row overflow-x-auto gap-2.5 md:gap-1 md:flex-col flex-1 md:flex-none md:w-40 md:min-h-[32rem] md:max-h-[32rem] dark:text-gray-200 text-sm font-medium text-left mb-1 md:mb-0 -translate-y-1"
|
||||
>
|
||||
@@ -574,11 +577,13 @@
|
||||
placeholder={$i18n.t('Search')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if visibleTabs.length > 0}
|
||||
{#each visibleTabs as tabId (tabId)}
|
||||
{#if tabId === 'general'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-general"
|
||||
aria-selected={selectedTab === 'general'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'general'
|
||||
@@ -596,6 +601,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
@@ -611,6 +617,9 @@
|
||||
</button>
|
||||
{:else if tabId === 'interface'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-interface"
|
||||
aria-selected={selectedTab === 'interface'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'interface'
|
||||
@@ -628,6 +637,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
@@ -644,6 +654,9 @@
|
||||
{:else if tabId === 'connections'}
|
||||
{#if $user?.role === 'admin' || ($user?.role === 'user' && $config?.features?.enable_direct_connections)}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-connections"
|
||||
aria-selected={selectedTab === 'connections'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'connections'
|
||||
@@ -661,6 +674,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
@@ -674,8 +688,11 @@
|
||||
</button>
|
||||
{/if}
|
||||
{:else if tabId === 'tools'}
|
||||
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)}
|
||||
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers && $config?.features?.direct_tool_servers)}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-tools"
|
||||
aria-selected={selectedTab === 'tools'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'tools'
|
||||
@@ -693,6 +710,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
@@ -709,6 +727,9 @@
|
||||
{/if}
|
||||
{:else if tabId === 'personalization'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-personalization"
|
||||
aria-selected={selectedTab === 'personalization'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'personalization'
|
||||
@@ -730,6 +751,9 @@
|
||||
</button>
|
||||
{:else if tabId === 'audio'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-audio"
|
||||
aria-selected={selectedTab === 'audio'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'audio'
|
||||
@@ -747,6 +771,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
@@ -763,6 +788,9 @@
|
||||
</button>
|
||||
{:else if tabId === 'chats'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-chats"
|
||||
aria-selected={selectedTab === 'chats'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'chats'
|
||||
@@ -780,6 +808,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
@@ -795,6 +824,9 @@
|
||||
</button>
|
||||
{:else if tabId === 'account'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-account"
|
||||
aria-selected={selectedTab === 'account'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'account'
|
||||
@@ -812,6 +844,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
@@ -827,6 +860,9 @@
|
||||
</button>
|
||||
{:else if tabId === 'about'}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-about"
|
||||
aria-selected={selectedTab === 'about'}
|
||||
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
|
||||
${
|
||||
selectedTab === 'about'
|
||||
@@ -844,6 +880,7 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
@@ -864,7 +901,6 @@
|
||||
{$i18n.t('No results found')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin'}
|
||||
<a
|
||||
href="/admin/settings"
|
||||
@@ -880,9 +916,9 @@
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { getContext } from 'svelte';
|
||||
import Modal from '../common/Modal.svelte';
|
||||
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -75,6 +76,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Only active when the chat input is in focus and an LLM is generating a response.'
|
||||
)}
|
||||
>
|
||||
{$i18n.t('Stop Generating')}<span class="text-xs"> *</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
||||
>
|
||||
Esc
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">{$i18n.t('Copy last code block')}</div>
|
||||
|
||||
@@ -123,6 +144,40 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full flex justify-between items-center">
|
||||
<div class=" text-sm">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'Only active when "Paste Large Text as File" setting is toggled on.'
|
||||
)}
|
||||
>
|
||||
{$i18n.t('Prevent file creation')}<span class="text-s"> *</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-1 text-xs">
|
||||
<div
|
||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
||||
>
|
||||
Ctrl/⌘
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
||||
>
|
||||
Shift
|
||||
</div>
|
||||
|
||||
<div
|
||||
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
|
||||
>
|
||||
V
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">{$i18n.t('Generate prompt pair')}</div>
|
||||
|
||||
@@ -146,9 +201,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">{$i18n.t('Toggle search')}</div>
|
||||
|
||||
@@ -251,6 +304,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-5 pb-4 text-xs text-gray-500 dark:text-gray-400">
|
||||
{$i18n.t(
|
||||
'Shortcuts with an asterisk (*) are situational and only active under specific conditions.'
|
||||
)}
|
||||
</div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-5">
|
||||
<div class=" text-lg font-medium self-center">{$i18n.t('Input commands')}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user