mirror of
https://github.com/open-webui/open-webui
synced 2025-06-23 02:16:52 +00:00
1161 lines
30 KiB
Svelte
1161 lines
30 KiB
Svelte
<script lang="ts">
|
|
import { config, models, settings, user } from '$lib/stores';
|
|
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
|
import { toast } from 'svelte-sonner';
|
|
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
|
import { updateUserInfo } from '$lib/apis/users';
|
|
import { getUserPosition } from '$lib/utils';
|
|
const dispatch = createEventDispatcher();
|
|
|
|
const i18n = getContext('i18n');
|
|
|
|
export let saveSettings: Function;
|
|
|
|
let backgroundImageUrl = null;
|
|
let inputFiles = null;
|
|
let filesInputElement;
|
|
|
|
// Addons
|
|
let titleAutoGenerate = true;
|
|
let autoFollowUps = true;
|
|
let autoTags = true;
|
|
|
|
let responseAutoCopy = false;
|
|
let widescreenMode = false;
|
|
let splitLargeChunks = false;
|
|
let scrollOnBranchChange = true;
|
|
let userLocation = false;
|
|
|
|
// Interface
|
|
let defaultModelId = '';
|
|
let showUsername = false;
|
|
|
|
let notificationSound = true;
|
|
let notificationSoundAlways = false;
|
|
|
|
let highContrastMode = false;
|
|
|
|
let detectArtifacts = true;
|
|
|
|
let richTextInput = true;
|
|
let promptAutocomplete = false;
|
|
|
|
let largeTextAsFile = false;
|
|
|
|
let landingPageMode = '';
|
|
let chatBubble = true;
|
|
let chatDirection: 'LTR' | 'RTL' | 'auto' = 'auto';
|
|
let ctrlEnterToSend = false;
|
|
let copyFormatted = false;
|
|
|
|
let collapseCodeBlocks = false;
|
|
let expandDetails = false;
|
|
|
|
let imageCompression = false;
|
|
let imageCompressionSize = {
|
|
width: '',
|
|
height: ''
|
|
};
|
|
|
|
// chat export
|
|
let stylizedPdfExport = true;
|
|
|
|
// Admin - Show Update Available Toast
|
|
let showUpdateToast = true;
|
|
let showChangelog = true;
|
|
|
|
let showEmojiInCall = false;
|
|
let voiceInterruption = false;
|
|
let hapticFeedback = false;
|
|
|
|
let webSearch = null;
|
|
|
|
let iframeSandboxAllowSameOrigin = false;
|
|
let iframeSandboxAllowForms = false;
|
|
|
|
const toggleExpandDetails = () => {
|
|
expandDetails = !expandDetails;
|
|
saveSettings({ expandDetails });
|
|
};
|
|
|
|
const toggleCollapseCodeBlocks = () => {
|
|
collapseCodeBlocks = !collapseCodeBlocks;
|
|
saveSettings({ collapseCodeBlocks });
|
|
};
|
|
|
|
const toggleSplitLargeChunks = async () => {
|
|
splitLargeChunks = !splitLargeChunks;
|
|
saveSettings({ splitLargeChunks: splitLargeChunks });
|
|
};
|
|
|
|
const toggleHighContrastMode = async () => {
|
|
highContrastMode = !highContrastMode;
|
|
saveSettings({ highContrastMode: highContrastMode });
|
|
};
|
|
|
|
const togglePromptAutocomplete = async () => {
|
|
promptAutocomplete = !promptAutocomplete;
|
|
saveSettings({ promptAutocomplete: promptAutocomplete });
|
|
};
|
|
|
|
const togglesScrollOnBranchChange = async () => {
|
|
scrollOnBranchChange = !scrollOnBranchChange;
|
|
saveSettings({ scrollOnBranchChange: scrollOnBranchChange });
|
|
};
|
|
|
|
const toggleWidescreenMode = async () => {
|
|
widescreenMode = !widescreenMode;
|
|
saveSettings({ widescreenMode: widescreenMode });
|
|
};
|
|
|
|
const toggleChatBubble = async () => {
|
|
chatBubble = !chatBubble;
|
|
saveSettings({ chatBubble: chatBubble });
|
|
};
|
|
|
|
const toggleLandingPageMode = async () => {
|
|
landingPageMode = landingPageMode === '' ? 'chat' : '';
|
|
saveSettings({ landingPageMode: landingPageMode });
|
|
};
|
|
|
|
const toggleShowUpdateToast = async () => {
|
|
showUpdateToast = !showUpdateToast;
|
|
saveSettings({ showUpdateToast: showUpdateToast });
|
|
};
|
|
|
|
const toggleNotificationSound = async () => {
|
|
notificationSound = !notificationSound;
|
|
saveSettings({ notificationSound: notificationSound });
|
|
};
|
|
|
|
const toggleNotificationSoundAlways = async () => {
|
|
notificationSoundAlways = !notificationSoundAlways;
|
|
saveSettings({ notificationSoundAlways: notificationSoundAlways });
|
|
};
|
|
|
|
const toggleShowChangelog = async () => {
|
|
showChangelog = !showChangelog;
|
|
saveSettings({ showChangelog: showChangelog });
|
|
};
|
|
|
|
const toggleShowUsername = async () => {
|
|
showUsername = !showUsername;
|
|
saveSettings({ showUsername: showUsername });
|
|
};
|
|
|
|
const toggleEmojiInCall = async () => {
|
|
showEmojiInCall = !showEmojiInCall;
|
|
saveSettings({ showEmojiInCall: showEmojiInCall });
|
|
};
|
|
|
|
const toggleVoiceInterruption = async () => {
|
|
voiceInterruption = !voiceInterruption;
|
|
saveSettings({ voiceInterruption: voiceInterruption });
|
|
};
|
|
|
|
const toggleImageCompression = async () => {
|
|
imageCompression = !imageCompression;
|
|
saveSettings({ imageCompression });
|
|
};
|
|
|
|
const toggleHapticFeedback = async () => {
|
|
hapticFeedback = !hapticFeedback;
|
|
saveSettings({ hapticFeedback: hapticFeedback });
|
|
};
|
|
|
|
const toggleStylizedPdfExport = async () => {
|
|
stylizedPdfExport = !stylizedPdfExport;
|
|
saveSettings({ stylizedPdfExport: stylizedPdfExport });
|
|
};
|
|
|
|
const toggleUserLocation = async () => {
|
|
userLocation = !userLocation;
|
|
|
|
if (userLocation) {
|
|
const position = await getUserPosition().catch((error) => {
|
|
toast.error(error.message);
|
|
return null;
|
|
});
|
|
|
|
if (position) {
|
|
await updateUserInfo(localStorage.token, { location: position });
|
|
toast.success($i18n.t('User location successfully retrieved.'));
|
|
} else {
|
|
userLocation = false;
|
|
}
|
|
}
|
|
|
|
saveSettings({ userLocation });
|
|
};
|
|
|
|
const toggleTitleAutoGenerate = async () => {
|
|
titleAutoGenerate = !titleAutoGenerate;
|
|
saveSettings({
|
|
title: {
|
|
...$settings.title,
|
|
auto: titleAutoGenerate
|
|
}
|
|
});
|
|
};
|
|
|
|
const toggleAutoFollowUps = async () => {
|
|
autoFollowUps = !autoFollowUps;
|
|
saveSettings({ autoFollowUps });
|
|
};
|
|
|
|
const toggleAutoTags = async () => {
|
|
autoTags = !autoTags;
|
|
saveSettings({ autoTags });
|
|
};
|
|
|
|
const toggleDetectArtifacts = async () => {
|
|
detectArtifacts = !detectArtifacts;
|
|
saveSettings({ detectArtifacts });
|
|
};
|
|
|
|
const toggleRichTextInput = async () => {
|
|
richTextInput = !richTextInput;
|
|
saveSettings({ richTextInput });
|
|
};
|
|
|
|
const toggleLargeTextAsFile = async () => {
|
|
largeTextAsFile = !largeTextAsFile;
|
|
saveSettings({ largeTextAsFile });
|
|
};
|
|
|
|
const toggleResponseAutoCopy = async () => {
|
|
const permission = await navigator.clipboard
|
|
.readText()
|
|
.then(() => {
|
|
return 'granted';
|
|
})
|
|
.catch(() => {
|
|
return '';
|
|
});
|
|
|
|
console.log(permission);
|
|
|
|
if (permission === 'granted') {
|
|
responseAutoCopy = !responseAutoCopy;
|
|
saveSettings({ responseAutoCopy: responseAutoCopy });
|
|
} else {
|
|
toast.error(
|
|
$i18n.t(
|
|
'Clipboard write permission denied. Please check your browser settings to grant the necessary access.'
|
|
)
|
|
);
|
|
}
|
|
};
|
|
|
|
const toggleCopyFormatted = async () => {
|
|
copyFormatted = !copyFormatted;
|
|
saveSettings({ copyFormatted });
|
|
};
|
|
|
|
const toggleChangeChatDirection = async () => {
|
|
if (chatDirection === 'auto') {
|
|
chatDirection = 'LTR';
|
|
} else if (chatDirection === 'LTR') {
|
|
chatDirection = 'RTL';
|
|
} else if (chatDirection === 'RTL') {
|
|
chatDirection = 'auto';
|
|
}
|
|
saveSettings({ chatDirection });
|
|
};
|
|
|
|
const togglectrlEnterToSend = async () => {
|
|
ctrlEnterToSend = !ctrlEnterToSend;
|
|
saveSettings({ ctrlEnterToSend });
|
|
};
|
|
|
|
const updateInterfaceHandler = async () => {
|
|
saveSettings({
|
|
models: [defaultModelId],
|
|
imageCompressionSize: imageCompressionSize
|
|
});
|
|
};
|
|
|
|
const toggleWebSearch = async () => {
|
|
webSearch = webSearch === null ? 'always' : null;
|
|
saveSettings({ webSearch: webSearch });
|
|
};
|
|
|
|
const toggleIframeSandboxAllowSameOrigin = async () => {
|
|
iframeSandboxAllowSameOrigin = !iframeSandboxAllowSameOrigin;
|
|
saveSettings({ iframeSandboxAllowSameOrigin });
|
|
};
|
|
|
|
const toggleIframeSandboxAllowForms = async () => {
|
|
iframeSandboxAllowForms = !iframeSandboxAllowForms;
|
|
saveSettings({ iframeSandboxAllowForms });
|
|
};
|
|
|
|
onMount(async () => {
|
|
titleAutoGenerate = $settings?.title?.auto ?? true;
|
|
autoTags = $settings?.autoTags ?? true;
|
|
autoFollowUps = $settings?.autoFollowUps ?? true;
|
|
|
|
highContrastMode = $settings?.highContrastMode ?? false;
|
|
|
|
detectArtifacts = $settings?.detectArtifacts ?? true;
|
|
responseAutoCopy = $settings?.responseAutoCopy ?? false;
|
|
|
|
showUsername = $settings?.showUsername ?? false;
|
|
showUpdateToast = $settings?.showUpdateToast ?? true;
|
|
showChangelog = $settings?.showChangelog ?? true;
|
|
|
|
showEmojiInCall = $settings?.showEmojiInCall ?? false;
|
|
voiceInterruption = $settings?.voiceInterruption ?? false;
|
|
|
|
richTextInput = $settings?.richTextInput ?? true;
|
|
promptAutocomplete = $settings?.promptAutocomplete ?? false;
|
|
largeTextAsFile = $settings?.largeTextAsFile ?? false;
|
|
copyFormatted = $settings?.copyFormatted ?? false;
|
|
|
|
collapseCodeBlocks = $settings?.collapseCodeBlocks ?? false;
|
|
expandDetails = $settings?.expandDetails ?? false;
|
|
|
|
landingPageMode = $settings?.landingPageMode ?? '';
|
|
chatBubble = $settings?.chatBubble ?? true;
|
|
widescreenMode = $settings?.widescreenMode ?? false;
|
|
splitLargeChunks = $settings?.splitLargeChunks ?? false;
|
|
scrollOnBranchChange = $settings?.scrollOnBranchChange ?? true;
|
|
chatDirection = $settings?.chatDirection ?? 'auto';
|
|
userLocation = $settings?.userLocation ?? false;
|
|
|
|
notificationSound = $settings?.notificationSound ?? true;
|
|
notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
|
|
|
|
iframeSandboxAllowSameOrigin = $settings?.iframeSandboxAllowSameOrigin ?? false;
|
|
iframeSandboxAllowForms = $settings?.iframeSandboxAllowForms ?? false;
|
|
|
|
stylizedPdfExport = $settings?.stylizedPdfExport ?? true;
|
|
|
|
hapticFeedback = $settings?.hapticFeedback ?? false;
|
|
ctrlEnterToSend = $settings?.ctrlEnterToSend ?? false;
|
|
|
|
imageCompression = $settings?.imageCompression ?? false;
|
|
imageCompressionSize = $settings?.imageCompressionSize ?? { width: '', height: '' };
|
|
|
|
defaultModelId = $settings?.models?.at(0) ?? '';
|
|
if ($config?.default_models) {
|
|
defaultModelId = $config.default_models.split(',')[0];
|
|
}
|
|
|
|
backgroundImageUrl = $settings?.backgroundImageUrl ?? null;
|
|
webSearch = $settings?.webSearch ?? null;
|
|
});
|
|
</script>
|
|
|
|
<form
|
|
id="tab-interface"
|
|
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
|
on:submit|preventDefault={() => {
|
|
updateInterfaceHandler();
|
|
dispatch('save');
|
|
}}
|
|
>
|
|
<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 overflow-y-scroll max-h-[28rem] lg:max-h-full">
|
|
<div>
|
|
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
|
|
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">
|
|
{$i18n.t('High Contrast Mode')} ({$i18n.t('Beta')})
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleHighContrastMode();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if highContrastMode === 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('Landing Page Mode')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleLandingPageMode();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if landingPageMode === ''}
|
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
|
{:else}
|
|
<span class="ml-2 self-center">{$i18n.t('Chat')}</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 Bubble UI')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleChatBubble();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if chatBubble === 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 !$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-sm 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 class=" py-0.5 flex w-full justify-between">
|
|
<div 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();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if widescreenMode === 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 direction')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={toggleChangeChatDirection}
|
|
type="button"
|
|
>
|
|
{#if chatDirection === 'LTR'}
|
|
<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
|
|
{:else if chatDirection === 'RTL'}
|
|
<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
|
|
{:else}
|
|
<span class="ml-2 self-center">{$i18n.t('Auto')}</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">
|
|
{$i18n.t('Notification Sound')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleNotificationSound();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if notificationSound === 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 notificationSound}
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">
|
|
{$i18n.t('Always Play Notification Sound')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleNotificationSoundAlways();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if notificationSoundAlways === 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}
|
|
|
|
{#if $user?.role === 'admin'}
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">
|
|
{$i18n.t('Toast notifications for new updates')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleShowUpdateToast();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if showUpdateToast === 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(`Show "What's New" modal on login`)}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleShowChangelog();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if showChangelog === 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 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
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleTitleAutoGenerate();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if titleAutoGenerate === 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('Follow-Up Auto-Generation')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleAutoFollowUps();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if autoFollowUps === 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 Tags Auto-Generation')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleAutoTags();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if autoTags === 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('Detect Artifacts Automatically')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleDetectArtifacts();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if detectArtifacts === 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('Auto-Copy Response to Clipboard')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleResponseAutoCopy();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if responseAutoCopy === 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('Rich Text Input for Chat')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleRichTextInput();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if richTextInput === 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 $config?.features?.enable_autocomplete_generation && richTextInput}
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">
|
|
{$i18n.t('Prompt Autocompletion')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
togglePromptAutocomplete();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if promptAutocomplete === 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 class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">
|
|
{$i18n.t('Paste Large Text as File')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleLargeTextAsFile();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if largeTextAsFile === 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('Copy Formatted Text')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleCopyFormatted();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if copyFormatted === 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('Always Collapse Code Blocks')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleCollapseCodeBlocks();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if collapseCodeBlocks === 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('Always Expand Details')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleExpandDetails();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if expandDetails === 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-sm 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>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleUserLocation();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if userLocation === 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('Haptic Feedback')} ({$i18n.t('Android')})
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleHapticFeedback();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if hapticFeedback === 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('Fluidly stream large external response chunks')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm 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('Enter Key Behavior')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded transition"
|
|
on:click={() => {
|
|
togglectrlEnterToSend();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if ctrlEnterToSend === true}
|
|
<span class="ml-2 self-center">{$i18n.t('Ctrl+Enter to Send')}</span>
|
|
{:else}
|
|
<span class="ml-2 self-center">{$i18n.t('Enter to Send')}</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">
|
|
{$i18n.t('Scroll On Branch Change')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
togglesScrollOnBranchChange();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if scrollOnBranchChange === 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('Web Search in Chat')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleWebSearch();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if webSearch === 'always'}
|
|
<span class="ml-2 self-center">{$i18n.t('Always')}</span>
|
|
{:else}
|
|
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
|
|
{/if}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleIframeSandboxAllowSameOrigin();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if iframeSandboxAllowSameOrigin === 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('iframe Sandbox Allow Forms')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleIframeSandboxAllowForms();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if iframeSandboxAllowForms === 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('Stylized PDF Export')}
|
|
</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleStylizedPdfExport();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if stylizedPdfExport === 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 class=" my-1.5 text-sm font-medium">{$i18n.t('Voice')}</div>
|
|
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleVoiceInterruption();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if voiceInterruption === 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('Display Emoji in Call')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleEmojiInCall();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if showEmojiInCall === 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 class=" my-1.5 text-sm font-medium">{$i18n.t('File')}</div>
|
|
|
|
<div>
|
|
<div class=" py-0.5 flex w-full justify-between">
|
|
<div class=" self-center text-xs">{$i18n.t('Image Compression')}</div>
|
|
|
|
<button
|
|
class="p-1 px-3 text-xs flex rounded-sm transition"
|
|
on:click={() => {
|
|
toggleImageCompression();
|
|
}}
|
|
type="button"
|
|
>
|
|
{#if imageCompression === 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 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>
|
|
<input
|
|
bind:value={imageCompressionSize.width}
|
|
type="number"
|
|
class="w-20 bg-transparent outline-hidden text-center"
|
|
min="0"
|
|
placeholder="Width"
|
|
/>x
|
|
<input
|
|
bind:value={imageCompressionSize.height}
|
|
type="number"
|
|
class="w-20 bg-transparent outline-hidden text-center"
|
|
min="0"
|
|
placeholder="Height"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex justify-end text-sm font-medium">
|
|
<button
|
|
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
|
type="submit"
|
|
>
|
|
{$i18n.t('Save')}
|
|
</button>
|
|
</div>
|
|
</form>
|