mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-28 05:27:35 +00:00
refac
This commit is contained in:
parent
db934a3b4f
commit
ba067258de
39 changed files with 1540 additions and 1442 deletions
|
|
@ -3,7 +3,6 @@
|
|||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import { verifyOpenAIConnection } from '$lib/apis/openai';
|
||||
import { verifyOllamaConnection } from '$lib/apis/ollama';
|
||||
|
||||
|
|
@ -59,9 +58,9 @@
|
|||
let loading = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
|
||||
const highContrastInputClass =
|
||||
const inputClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const highContrastSelectClass =
|
||||
const selectClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
|
||||
|
||||
const verifyOllamaHandler = async () => {
|
||||
|
|
@ -311,14 +310,13 @@
|
|||
<label
|
||||
for="url-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
>{$i18n.t('URL')}</label
|
||||
`}>{$i18n.t('URL')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
id="url-input"
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
|
|
@ -378,9 +376,7 @@
|
|||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="select-bearer-or-session"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="select-bearer-or-session" class={`text-xs text-gray-500`}
|
||||
>{$i18n.t('Auth')}</label
|
||||
>
|
||||
|
||||
|
|
@ -388,7 +384,7 @@
|
|||
<div class="flex-shrink-0 self-start">
|
||||
<select
|
||||
id="select-bearer-or-session"
|
||||
class={`w-full text-sm pr-5 ${($settings?.highContrastMode ?? false) ? highContrastSelectClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 dark:bg-gray-900'}`}
|
||||
class={`w-full text-sm pr-5 ${selectClass}`}
|
||||
bind:value={auth_type}
|
||||
>
|
||||
<option value="none">{$i18n.t('None')}</option>
|
||||
|
|
@ -412,27 +408,19 @@
|
|||
required={false}
|
||||
/>
|
||||
{:else if auth_type === 'none'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('No authentication')}
|
||||
</div>
|
||||
{:else if auth_type === 'session'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('Forwards system user session credentials to authenticate')}
|
||||
</div>
|
||||
{:else if auth_type === 'system_oauth'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
||||
</div>
|
||||
{:else if ['azure_ad', 'microsoft_entra_id'].includes(auth_type)}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('Uses DefaultAzureCredential to authenticate')}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -447,8 +435,7 @@
|
|||
<label
|
||||
for="headers-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
>{$i18n.t('Headers')}</label
|
||||
`}>{$i18n.t('Headers')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
|
|
@ -475,8 +462,7 @@
|
|||
<label
|
||||
for="prefix-id-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
>{$i18n.t('Prefix ID')}</label
|
||||
`}>{$i18n.t('Prefix ID')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
|
|
@ -486,7 +472,7 @@
|
|||
)}
|
||||
>
|
||||
<input
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
id="prefix-id-input"
|
||||
bind:value={prefixId}
|
||||
|
|
@ -503,8 +489,7 @@
|
|||
<label
|
||||
for="provider-select"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
>{$i18n.t('Provider')}</label
|
||||
`}>{$i18n.t('Provider')}</label
|
||||
>
|
||||
|
||||
<div>
|
||||
|
|
@ -527,14 +512,13 @@
|
|||
<label
|
||||
for="api-version-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
>{$i18n.t('API Version')}</label
|
||||
`}>{$i18n.t('API Version')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
id="api-version-input"
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={apiVersion}
|
||||
placeholder={$i18n.t('API Version')}
|
||||
|
|
@ -551,8 +535,7 @@
|
|||
<label
|
||||
for="api-type-toggle"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
>{$i18n.t('API Type')}</label
|
||||
`}>{$i18n.t('API Type')}</label
|
||||
>
|
||||
|
||||
<div>
|
||||
|
|
@ -571,7 +554,8 @@
|
|||
'This feature is currently experimental and may not work as expected.'
|
||||
)}
|
||||
>
|
||||
<span class=" text-gray-400 dark:text-gray-600"
|
||||
<span
|
||||
class="inline-flex items-center text-[0.625rem] font-normal uppercase leading-none text-gray-400 dark:text-gray-600"
|
||||
>{$i18n.t('Experimental')}</span
|
||||
>
|
||||
|
||||
|
|
@ -589,7 +573,7 @@
|
|||
<div class="mb-1 flex justify-between">
|
||||
<div
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
`}
|
||||
>
|
||||
{$i18n.t('Model IDs')}
|
||||
</div>
|
||||
|
|
@ -621,7 +605,7 @@
|
|||
{:else}
|
||||
<div
|
||||
class={`text-gray-500 text-xs text-center py-2 px-10
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
`}
|
||||
>
|
||||
{#if ollama}
|
||||
{$i18n.t('Leave empty to include all models from "{{url}}/api/tags" endpoint', {
|
||||
|
|
@ -644,7 +628,7 @@
|
|||
<div class="flex items-center">
|
||||
<label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label>
|
||||
<input
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : `rounded-lg bg-transparent py-1 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden ${modelId ? '' : 'text-gray-500'}`}`}
|
||||
class={`w-full text-sm ${inputClass} ${modelId ? '' : 'text-gray-500'}`}
|
||||
bind:value={modelId}
|
||||
id="add-model-id-input"
|
||||
placeholder={$i18n.t('Add a model ID')}
|
||||
|
|
@ -668,7 +652,7 @@
|
|||
<div class="flex flex-col w-full">
|
||||
<div
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
`}
|
||||
>
|
||||
{$i18n.t('Tags')}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,6 @@
|
|||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext<any>('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
|
@ -61,9 +59,9 @@
|
|||
let loadingPolicy = false;
|
||||
let policyLoadError = '';
|
||||
|
||||
const highContrastInputClass =
|
||||
const inputClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const highContrastSelectClass =
|
||||
const selectClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
|
||||
|
||||
const stringifyJson = (value: object | null | undefined) => {
|
||||
|
|
@ -399,9 +397,7 @@
|
|||
<div class="flex gap-2">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<label
|
||||
for="terminal-name"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="terminal-name" class={`text-xs text-gray-500`}
|
||||
>{$i18n.t('Name')}</label
|
||||
>
|
||||
</div>
|
||||
|
|
@ -409,7 +405,7 @@
|
|||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="terminal-name"
|
||||
class={`w-full flex-1 text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('My Terminal')}
|
||||
|
|
@ -420,9 +416,7 @@
|
|||
{#if !direct}
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<label
|
||||
for="terminal-id"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="terminal-id" class={`text-xs text-gray-500`}
|
||||
>{$i18n.t('ID')}
|
||||
<span class="opacity-50">({$i18n.t('optional')})</span></label
|
||||
>
|
||||
|
|
@ -430,7 +424,7 @@
|
|||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="terminal-id"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={id}
|
||||
placeholder="auto"
|
||||
|
|
@ -444,17 +438,13 @@
|
|||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<label
|
||||
for="terminal-url"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('URL')}</label
|
||||
>
|
||||
<label for="terminal-url" class={`text-xs text-gray-500`}>{$i18n.t('URL')}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="terminal-url"
|
||||
class={`w-full flex-1 text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder="http://localhost:9900"
|
||||
|
|
@ -519,16 +509,14 @@
|
|||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Policy ID')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="policy-id"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={policyId}
|
||||
placeholder="python-ds"
|
||||
|
|
@ -549,9 +537,7 @@
|
|||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Image')}
|
||||
<span class="opacity-50">({$i18n.t('optional')})</span>
|
||||
</div>
|
||||
|
|
@ -559,7 +545,7 @@
|
|||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="policy-image"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={policyImage}
|
||||
placeholder="ghcr.io/open-webui/open-terminal:latest"
|
||||
|
|
@ -572,16 +558,14 @@
|
|||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('CPU')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="policy-cpu"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={policyCpu}
|
||||
placeholder="1"
|
||||
|
|
@ -591,16 +575,14 @@
|
|||
</div>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Memory')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="policy-memory"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={policyMemory}
|
||||
placeholder="1Gi"
|
||||
|
|
@ -613,16 +595,14 @@
|
|||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Storage')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-shrink-0 self-start">
|
||||
<select
|
||||
class={`w-full text-sm pr-5 ${($settings?.highContrastMode ?? false) ? highContrastSelectClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 dark:bg-gray-900'}`}
|
||||
class={`w-full text-sm pr-5 ${selectClass}`}
|
||||
bind:value={policyStorage}
|
||||
>
|
||||
<option value="ephemeral">{$i18n.t('Ephemeral')}</option>
|
||||
|
|
@ -633,7 +613,7 @@
|
|||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="policy-storage-size"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={policyStorageSize}
|
||||
placeholder="5Gi"
|
||||
|
|
@ -646,9 +626,7 @@
|
|||
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Idle Timeout')}
|
||||
<span class="opacity-50">({$i18n.t('min')})</span>
|
||||
</div>
|
||||
|
|
@ -656,7 +634,7 @@
|
|||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="idle-timeout"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="number"
|
||||
min="0"
|
||||
bind:value={policyIdleTimeout}
|
||||
|
|
@ -671,9 +649,7 @@
|
|||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Environment Variables')}
|
||||
</div>
|
||||
<button
|
||||
|
|
@ -688,13 +664,13 @@
|
|||
{#each policyEnvPairs as pair, idx}
|
||||
<div class="flex gap-1.5 mb-1">
|
||||
<input
|
||||
class={`flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={pair.key}
|
||||
placeholder="KEY"
|
||||
/>
|
||||
<input
|
||||
class={`flex-[2] text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`flex-[2] text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={pair.value}
|
||||
placeholder="value"
|
||||
|
|
@ -715,15 +691,13 @@
|
|||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Lifecycle JSON')}
|
||||
</div>
|
||||
</div>
|
||||
<textarea
|
||||
id="lifecycle-json"
|
||||
class={`w-full min-h-24 resize-y text-xs font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'rounded-lg border border-gray-200 bg-transparent p-2 outline-hidden placeholder:text-gray-300 dark:border-gray-800 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full min-h-24 resize-y text-xs font-mono ${inputClass}`}
|
||||
bind:value={lifecycleJson}
|
||||
spellcheck="false"
|
||||
placeholder={`{\n "reset": {\n "schedule": "@weekly",\n "timezone": "UTC"\n }\n}`}
|
||||
|
|
@ -794,9 +768,7 @@
|
|||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('OpenAPI Spec')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -809,7 +781,7 @@
|
|||
>{$i18n.t('openapi.json URL or Path')}</label
|
||||
>
|
||||
<input
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
id="openapi-path"
|
||||
bind:value={path}
|
||||
|
|
@ -821,9 +793,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs mt-1 text-gray-500`}>
|
||||
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
|
||||
url: path.includes('://')
|
||||
? path
|
||||
|
|
@ -838,9 +808,7 @@
|
|||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Auth')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -848,10 +816,7 @@
|
|||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-shrink-0 self-start">
|
||||
<select
|
||||
class={`w-full text-sm pr-5 ${($settings?.highContrastMode ?? false) ? highContrastSelectClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 dark:bg-gray-900'}`}
|
||||
bind:value={auth_type}
|
||||
>
|
||||
<select class={`w-full text-sm pr-5 ${selectClass}`} bind:value={auth_type}>
|
||||
<option value="none">{$i18n.t('None')}</option>
|
||||
<option value="bearer">{$i18n.t('Bearer')}</option>
|
||||
{#if !direct}
|
||||
|
|
@ -869,21 +834,15 @@
|
|||
required={false}
|
||||
/>
|
||||
{:else if auth_type === 'none'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('No authentication')}
|
||||
</div>
|
||||
{:else if auth_type === 'session'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('Forwards system user session credentials to authenticate')}
|
||||
</div>
|
||||
{:else if auth_type === 'system_oauth'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Minus from '$lib/components/icons/Minus.svelte';
|
||||
|
|
@ -70,9 +69,9 @@
|
|||
let showAccessControlModal = false;
|
||||
let showDeleteConfirmDialog = false;
|
||||
|
||||
const highContrastInputClass =
|
||||
const inputClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const highContrastSelectClass =
|
||||
const selectClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
|
||||
|
||||
const registerOAuthClientHandler = async () => {
|
||||
|
|
@ -538,17 +537,13 @@
|
|||
<div class="flex gap-2">
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<label
|
||||
for="enter-name"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('Name')}</label
|
||||
>
|
||||
<label for="enter-name" class={`text-xs text-gray-500`}>{$i18n.t('Name')}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="enter-name"
|
||||
class={`w-full flex-1 text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Enter name')}
|
||||
|
|
@ -559,9 +554,7 @@
|
|||
{#if !direct}
|
||||
<div class="flex flex-col flex-1">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<label
|
||||
for="enter-id"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="enter-id" class={`text-xs text-gray-500`}
|
||||
>{$i18n.t('ID')}
|
||||
{#if type !== 'mcp'}<span class="opacity-50">({$i18n.t('optional')})</span
|
||||
>{/if}</label
|
||||
|
|
@ -570,7 +563,7 @@
|
|||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="enter-id"
|
||||
class={`w-full flex-1 text-sm font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm font-mono ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={id}
|
||||
placeholder="auto"
|
||||
|
|
@ -583,16 +576,14 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-1 mb-1.5">
|
||||
<label
|
||||
for="description"
|
||||
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="description" class={`mb-0.5 text-xs text-gray-500`}
|
||||
>{$i18n.t('Description')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
id="description"
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Enter description')}
|
||||
|
|
@ -604,17 +595,13 @@
|
|||
<div class="flex gap-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<label
|
||||
for="api-base-url"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('URL')}</label
|
||||
>
|
||||
<label for="api-base-url" class={`text-xs text-gray-500`}>{$i18n.t('URL')}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="api-base-url"
|
||||
class={`w-full flex-1 text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
|
|
@ -661,10 +648,7 @@
|
|||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
for="select-bearer-or-session"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div for="select-bearer-or-session" class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Auth')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -710,7 +694,7 @@
|
|||
<div class="flex-shrink-0 self-start">
|
||||
<select
|
||||
id="select-bearer-or-session"
|
||||
class={`w-full text-sm pr-5 ${($settings?.highContrastMode ?? false) ? highContrastSelectClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 dark:bg-gray-900'}`}
|
||||
class={`w-full text-sm pr-5 ${selectClass}`}
|
||||
bind:value={auth_type}
|
||||
>
|
||||
<option value="none">{$i18n.t('None')}</option>
|
||||
|
|
@ -736,26 +720,20 @@
|
|||
required={false}
|
||||
/>
|
||||
{:else if auth_type === 'none'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('No authentication')}
|
||||
</div>
|
||||
{:else if auth_type === 'session'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('Forwards system user session credentials to authenticate')}
|
||||
</div>
|
||||
{:else if auth_type === 'system_oauth'}
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs self-center translate-y-[1px] text-gray-500`}>
|
||||
{$i18n.t('Forwards system user OAuth access token to authenticate')}
|
||||
</div>
|
||||
{:else if auth_type === 'oauth_2.1'}
|
||||
<div
|
||||
class={`flex items-center text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
class={`flex items-center text-xs self-center translate-y-[1px] text-gray-500`}
|
||||
>
|
||||
{$i18n.t('Uses OAuth 2.1 Dynamic Client Registration')}
|
||||
</div>
|
||||
|
|
@ -773,7 +751,7 @@
|
|||
/>
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={oauthServerUrl}
|
||||
placeholder={$i18n.t('OAuth Server URL')}
|
||||
|
|
@ -824,10 +802,7 @@
|
|||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<div class="flex gap-2 items-center">
|
||||
<div
|
||||
for="select-bearer-or-session"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div for="select-bearer-or-session" class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('OpenAPI Spec')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -837,7 +812,7 @@
|
|||
<div class="flex-shrink-0 self-start">
|
||||
<select
|
||||
id="select-bearer-or-session"
|
||||
class={`w-full text-sm pr-5 ${($settings?.highContrastMode ?? false) ? highContrastSelectClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 dark:bg-gray-900'}`}
|
||||
class={`w-full text-sm pr-5 ${selectClass}`}
|
||||
bind:value={spec_type}
|
||||
>
|
||||
<option value="url">{$i18n.t('URL')}</option>
|
||||
|
|
@ -852,7 +827,7 @@
|
|||
>{$i18n.t('openapi.json URL or Path')}</label
|
||||
>
|
||||
<input
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
id="url-or-path"
|
||||
bind:value={path}
|
||||
|
|
@ -862,12 +837,10 @@
|
|||
/>
|
||||
</div>
|
||||
{:else if spec_type === 'json'}
|
||||
<div
|
||||
class={`text-xs w-full self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs w-full self-center translate-y-[1px] text-gray-500`}>
|
||||
<label for="url-or-path" class="sr-only">{$i18n.t('JSON Spec')}</label>
|
||||
<textarea
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-black dark:text-white'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
bind:value={spec}
|
||||
placeholder={$i18n.t('JSON Spec')}
|
||||
autocomplete="off"
|
||||
|
|
@ -880,9 +853,7 @@
|
|||
</div>
|
||||
|
||||
{#if ['', 'url'].includes(spec_type)}
|
||||
<div
|
||||
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs mt-1 text-gray-500`}>
|
||||
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
|
||||
url: path.includes('://')
|
||||
? path
|
||||
|
|
@ -897,16 +868,14 @@
|
|||
{#if type === 'mcp' && ['oauth_2.1', 'oauth_2.1_static'].includes(auth_type)}
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="oauth-scope"
|
||||
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="oauth-scope" class={`mb-0.5 text-xs text-gray-500`}
|
||||
>{$i18n.t('OAuth Scopes')}</label
|
||||
>
|
||||
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
id="oauth-scope"
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={oauthScope}
|
||||
placeholder={$i18n.t('Use discovered scopes')}
|
||||
|
|
@ -918,16 +887,14 @@
|
|||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="oauth-resource-parameter"
|
||||
class={`mb-0.5 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="oauth-resource-parameter" class={`mb-0.5 text-xs text-gray-500`}
|
||||
>{$i18n.t('OAuth Resource Parameter')}</label
|
||||
>
|
||||
|
||||
<div class="flex flex-1 items-center">
|
||||
<select
|
||||
id="oauth-resource-parameter"
|
||||
class={`w-full text-sm pr-5 ${($settings?.highContrastMode ?? false) ? highContrastSelectClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 dark:bg-gray-900'}`}
|
||||
class={`w-full text-sm pr-5 ${selectClass}`}
|
||||
bind:value={oauthResourceParameter}
|
||||
>
|
||||
<option value="auto">{$i18n.t('Automatic')}</option>
|
||||
|
|
@ -945,8 +912,7 @@
|
|||
<label
|
||||
for="headers-input"
|
||||
class={`mb-0.5 text-xs text-gray-500
|
||||
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
|
||||
>{$i18n.t('Headers')}</label
|
||||
`}>{$i18n.t('Headers')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
|
|
@ -973,16 +939,14 @@
|
|||
<hr class=" border-gray-100/50 dark:border-gray-700/10 my-2.5 w-full" />
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<label
|
||||
for="function-name-filter-list"
|
||||
class={`mb-1 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="function-name-filter-list" class={`mb-1 text-xs text-gray-500`}
|
||||
>{$i18n.t('Function Name Filter List')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
id="function-name-filter-list"
|
||||
class={`w-full text-sm ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-sm ${inputClass}`}
|
||||
type="text"
|
||||
bind:value={functionNameFilterList}
|
||||
placeholder={$i18n.t('Enter function name filter list (e.g. func1, !func2)')}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
export let urlIdx: number | null = null;
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<Modal size="sm" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<div class="flex w-full justify-between items-center text-sm font-medium self-center">
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<Modal size="sm" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<div class=" text-sm font-medium self-center">
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
events: ['*'] as string[]
|
||||
};
|
||||
|
||||
const highContrastInputClass =
|
||||
const inputClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
|
||||
$: events = eventItems.map((item) => item.event);
|
||||
|
|
@ -392,7 +392,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showWebhookModal} size="sm">
|
||||
<Modal bind:show={showWebhookModal} size="sm" className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class="flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<h1 class="text-sm font-medium self-center">
|
||||
|
|
@ -413,14 +413,12 @@
|
|||
<form class="flex flex-col w-full" on:submit|preventDefault={saveWebhook}>
|
||||
<div class="flex gap-3">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="event-webhook-name"
|
||||
class={`text-xs mb-0.5 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="event-webhook-name" class={`text-xs mb-0.5 text-gray-500`}
|
||||
>{$i18n.t('Name')}</label
|
||||
>
|
||||
<input
|
||||
id="event-webhook-name"
|
||||
class={`w-full text-xs ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-xs ${inputClass}`}
|
||||
type="text"
|
||||
placeholder={$i18n.t('Identity audit')}
|
||||
autocomplete="off"
|
||||
|
|
@ -431,15 +429,13 @@
|
|||
|
||||
<div class="flex gap-3 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<label
|
||||
for="event-webhook-url"
|
||||
class={`text-xs mb-0.5 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="event-webhook-url" class={`text-xs mb-0.5 text-gray-500`}
|
||||
>{$i18n.t('URL')}</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<input
|
||||
id="event-webhook-url"
|
||||
class={`w-full text-xs ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full text-xs ${inputClass}`}
|
||||
type="url"
|
||||
placeholder="https://example.com/events"
|
||||
autocomplete="off"
|
||||
|
|
@ -455,9 +451,7 @@
|
|||
|
||||
<div class="mt-3">
|
||||
<div class="flex justify-between items-center gap-3">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
<div class={`text-xs text-gray-500`}>
|
||||
{$i18n.t('Send events for')}
|
||||
</div>
|
||||
<select
|
||||
|
|
@ -524,7 +518,8 @@
|
|||
|
||||
<div class="relative">
|
||||
<input
|
||||
class={`w-full text-xs ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
data-settings-search
|
||||
class={`w-full text-xs ${inputClass}`}
|
||||
type="text"
|
||||
placeholder={$i18n.t('Search users or groups')}
|
||||
autocomplete="off"
|
||||
|
|
@ -571,9 +566,7 @@
|
|||
|
||||
<div class="mt-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<label
|
||||
for="event-webhook-all-events"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
<label for="event-webhook-all-events" class={`text-xs text-gray-500`}
|
||||
>{$i18n.t('Events')}</label
|
||||
>
|
||||
<label class="flex items-center gap-1.5 text-xs text-gray-700 dark:text-gray-300">
|
||||
|
|
@ -610,7 +603,7 @@
|
|||
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
class={`w-full flex-1 text-xs font-mono ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
class={`w-full flex-1 text-xs font-mono ${inputClass}`}
|
||||
type="text"
|
||||
placeholder={$i18n.t('Search or add pattern')}
|
||||
autocomplete="off"
|
||||
|
|
|
|||
|
|
@ -408,7 +408,7 @@
|
|||
onMount(refresh);
|
||||
</script>
|
||||
|
||||
<Modal bind:show={showSourceModal} size="sm">
|
||||
<Modal bind:show={showSourceModal} size="sm" className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class="flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<h1 class="text-sm font-medium self-center">
|
||||
|
|
|
|||
|
|
@ -535,6 +535,7 @@
|
|||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
data-settings-search
|
||||
class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={searchValue}
|
||||
placeholder={$i18n.t('Search Models')}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<Modal size="sm" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<div class=" text-sm font-medium self-center">
|
||||
|
|
|
|||
|
|
@ -163,7 +163,7 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<Modal size="lg" bind:show>
|
||||
<Modal size="lg" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<div class=" text-sm font-medium self-center">
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@
|
|||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import UserSettingRow from './UserSettingRow.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -17,6 +19,8 @@
|
|||
current: '',
|
||||
latest: ''
|
||||
};
|
||||
const actionButtonClass =
|
||||
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
const checkForVersionUpdates = async () => {
|
||||
updateAvailable = null;
|
||||
|
|
@ -44,19 +48,15 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div id="tab-about" class="flex flex-col h-full justify-between space-y-3 text-sm">
|
||||
<div id="tab-about" class="flex flex-col h-full justify-between text-sm">
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$i18n.t('About')}</h2>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 space-y-3">
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2.5 flex space-x-2 items-center">
|
||||
<div>
|
||||
{$WEBUI_NAME}
|
||||
{$i18n.t('Version')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full justify-between items-center">
|
||||
<div class="flex flex-col text-xs text-gray-700 dark:text-gray-200">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<UserSettingSection title={`${$WEBUI_NAME} ${$i18n.t('Version')}`} first>
|
||||
<UserSettingRow
|
||||
description={$i18n.t('View the installed version and check release updates.')}
|
||||
>
|
||||
<div slot="label" class="flex flex-col text-xs text-gray-600 dark:text-gray-400">
|
||||
<div class="flex gap-1">
|
||||
<Tooltip content={WEBUI_BUILD_HASH}>
|
||||
v{WEBUI_VERSION}
|
||||
|
|
@ -77,7 +77,7 @@
|
|||
</div>
|
||||
|
||||
<button
|
||||
class=" underline flex items-center space-x-1 text-xs text-gray-500 dark:text-gray-500"
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
showChangelog.set(true);
|
||||
}}
|
||||
|
|
@ -88,7 +88,7 @@
|
|||
|
||||
{#if $config?.features?.enable_version_update_check}
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-100 hover:bg-gray-200 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-normal"
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
|
|
@ -96,85 +96,67 @@
|
|||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</UserSettingRow>
|
||||
</UserSettingSection>
|
||||
|
||||
{#if ollamaVersion}
|
||||
<hr class=" border-gray-100/20 dark:border-white/[0.025]" />
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2.5">
|
||||
{$i18n.t('Ollama Version')}
|
||||
<UserSettingSection title={$i18n.t('Ollama Version')}>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{ollamaVersion ?? 'N/A'}
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-gray-200">
|
||||
{ollamaVersion ?? 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
|
||||
<hr class=" border-gray-100/20 dark:border-white/[0.025]" />
|
||||
<UserSettingSection title={$i18n.t('Community')}>
|
||||
{#if $config?.license_metadata}
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||
<span>{$WEBUI_NAME}</span> -
|
||||
{/if}
|
||||
|
||||
{#if $config?.license_metadata}
|
||||
<div class="mb-2 text-xs">
|
||||
{#if !$WEBUI_NAME.includes('Open WebUI')}
|
||||
<span class=" text-gray-500 dark:text-gray-300 font-normal">{$WEBUI_NAME}</span> -
|
||||
{/if}
|
||||
<span class="capitalize">{$config?.license_metadata?.type}</span> license purchased by
|
||||
<span class="capitalize">{$config?.license_metadata?.organization_name}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-wrap gap-x-3 gap-y-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
<a
|
||||
class="hover:text-gray-700 dark:hover:text-gray-400"
|
||||
href="https://discord.gg/5rJgQTnV4s"
|
||||
target="_blank">Discord</a
|
||||
>
|
||||
<a
|
||||
class="hover:text-gray-700 dark:hover:text-gray-400"
|
||||
href="https://twitter.com/OpenWebUI"
|
||||
target="_blank">X</a
|
||||
>
|
||||
<a
|
||||
class="hover:text-gray-700 dark:hover:text-gray-400"
|
||||
href="https://github.com/open-webui/open-webui"
|
||||
target="_blank">GitHub</a
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class=" capitalize">{$config?.license_metadata?.type}</span> license purchased by
|
||||
<span class=" capitalize">{$config?.license_metadata?.organization_name}</span>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
Emoji graphics provided by
|
||||
<a href="https://github.com/jdecked/twemoji" target="_blank">Twemoji</a>, licensed under
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC-BY 4.0</a>.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex space-x-1">
|
||||
<a href="https://discord.gg/5rJgQTnV4s" target="_blank">
|
||||
<img
|
||||
alt="Discord"
|
||||
src="https://img.shields.io/badge/Discord-Open_WebUI-blue?logo=discord&logoColor=white"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/OpenWebUI" target="_blank">
|
||||
<img
|
||||
alt="X (formerly Twitter) Follow"
|
||||
src="https://img.shields.io/twitter/follow/OpenWebUI"
|
||||
/>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/open-webui/open-webui" target="_blank">
|
||||
<img
|
||||
alt="Github Repo"
|
||||
src="https://img.shields.io/github/stars/open-webui/open-webui?style=social&label=Star us on Github"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
Emoji graphics provided by
|
||||
<a href="https://github.com/jdecked/twemoji" target="_blank">Twemoji</a>, licensed under
|
||||
<a href="https://creativecommons.org/licenses/by/4.0/" target="_blank">CC-BY 4.0</a>.
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<pre
|
||||
class="text-xs text-gray-400 dark:text-gray-500">Copyright (c) {new Date().getFullYear()} <a
|
||||
href="https://openwebui.com"
|
||||
target="_blank"
|
||||
class="underline">Open WebUI Inc.</a
|
||||
> <a href="https://github.com/open-webui/open-webui/blob/main/LICENSE" target="_blank"
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
Copyright (c) {new Date().getFullYear()}
|
||||
<a href="https://openwebui.com" target="_blank" class="underline">Open WebUI Inc.</a>
|
||||
<a href="https://github.com/open-webui/open-webui/blob/main/LICENSE" target="_blank"
|
||||
>All rights reserved.</a
|
||||
>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Created by')}
|
||||
<a
|
||||
class=" text-gray-500 dark:text-gray-300 font-normal"
|
||||
href="https://github.com/tjbck"
|
||||
target="_blank">Timothy J. Baek</a
|
||||
>
|
||||
</div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Created by')}
|
||||
<a class="text-gray-500 dark:text-gray-400" href="https://github.com/tjbck" target="_blank"
|
||||
>Timothy J. Baek</a
|
||||
>
|
||||
</div>
|
||||
</UserSettingSection>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,16 +14,16 @@
|
|||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import User from '$lib/components/icons/User.svelte';
|
||||
import UserProfileImage from './Account/UserProfileImage.svelte';
|
||||
import UserSettingField from './UserSettingField.svelte';
|
||||
import UserSettingRow from './UserSettingRow.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
export let saveSettings: Function;
|
||||
|
||||
let loaded = false;
|
||||
|
||||
let profileImageUrl = '';
|
||||
let name = '';
|
||||
let bio = '';
|
||||
|
|
@ -39,7 +39,13 @@
|
|||
|
||||
let APIKey = '';
|
||||
let APIKeyCopied = false;
|
||||
let profileImageInputElement: HTMLInputElement;
|
||||
|
||||
const textareaClass =
|
||||
'w-full resize-y rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const inputClass =
|
||||
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const actionButtonClass =
|
||||
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (name !== $user?.name) {
|
||||
|
|
@ -119,18 +125,14 @@
|
|||
return '';
|
||||
});
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<div id="tab-account" class="flex h-full flex-col text-sm">
|
||||
<div class="flex-1 min-h-0 w-full overflow-y-auto scrollbar-hover pr-1.5 py-1">
|
||||
<div class="flex-1 min-h-0 w-full overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<h2 class="mb-4 text-sm font-medium text-gray-900 dark:text-white">{$i18n.t('Account')}</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- <div class=" text-sm font-normal">{$i18n.t('Account')}</div> -->
|
||||
|
||||
<UserSettingSection title={$i18n.t('Profile')} first>
|
||||
<UserProfileImage
|
||||
bind:profileImageUrl
|
||||
user={$user}
|
||||
|
|
@ -138,38 +140,39 @@
|
|||
displayName={$user?.name}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Name')}</div>
|
||||
|
||||
<UserSettingField
|
||||
label={$i18n.t('Name')}
|
||||
description={$i18n.t('Set the display name shown across your account.')}
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
class={inputClass}
|
||||
type="text"
|
||||
bind:value={name}
|
||||
aria-label={$i18n.t('Name')}
|
||||
required
|
||||
placeholder={$i18n.t('Enter your name')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Bio')}</div>
|
||||
</UserSettingField>
|
||||
|
||||
<UserSettingField
|
||||
label={$i18n.t('Bio')}
|
||||
description={$i18n.t('Add optional profile context visible where profiles are shown.')}
|
||||
>
|
||||
<Textarea
|
||||
className="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
className={textareaClass}
|
||||
minSize={60}
|
||||
bind:value={bio}
|
||||
ariaLabel={$i18n.t('Bio')}
|
||||
placeholder={$i18n.t('Share your background and interests')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Gender')}
|
||||
</div>
|
||||
</UserSettingField>
|
||||
|
||||
<UserSettingField
|
||||
label={$i18n.t('Gender')}
|
||||
description={$i18n.t('Choose the gender value stored on your profile.')}
|
||||
>
|
||||
<select
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden dark:text-gray-300"
|
||||
class={inputClass}
|
||||
bind:value={_gender}
|
||||
aria-label={$i18n.t('Gender')}
|
||||
on:change={(e) => {
|
||||
|
|
@ -191,7 +194,7 @@
|
|||
|
||||
{#if _gender === 'custom'}
|
||||
<input
|
||||
class="mt-1 w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
class="mt-1 {inputClass}"
|
||||
type="text"
|
||||
required
|
||||
aria-label={$i18n.t('Custom Gender')}
|
||||
|
|
@ -199,150 +202,81 @@
|
|||
bind:value={gender}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Birth Date')}
|
||||
</div>
|
||||
</UserSettingField>
|
||||
|
||||
<UserSettingField
|
||||
label={$i18n.t('Birth Date')}
|
||||
description={$i18n.t('Set the birth date saved with your profile.')}
|
||||
>
|
||||
<input
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
class={inputClass}
|
||||
type="date"
|
||||
aria-label={$i18n.t('Birth Date')}
|
||||
bind:value={dateOfBirth}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</UserSettingField>
|
||||
</UserSettingSection>
|
||||
|
||||
{#if $config?.features?.enable_user_webhooks && ($user?.role === 'admin' || ($user?.permissions?.features?.webhooks ?? false))}
|
||||
<div class="mt-3">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="mb-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Notification Webhook')}
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full bg-transparent py-1 text-[0.8125rem] text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
type="url"
|
||||
placeholder={$i18n.t('Enter your webhook URL')}
|
||||
aria-label={$i18n.t('Notification Webhook')}
|
||||
bind:value={webhookUrl}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingSection title={$i18n.t('Notifications')}>
|
||||
<UserSettingField
|
||||
label={$i18n.t('Notification Webhook')}
|
||||
description={$i18n.t('Send account notifications to this webhook URL.')}
|
||||
>
|
||||
<input
|
||||
class={inputClass}
|
||||
type="url"
|
||||
placeholder={$i18n.t('Enter your webhook URL')}
|
||||
aria-label={$i18n.t('Notification Webhook')}
|
||||
bind:value={webhookUrl}
|
||||
required
|
||||
/>
|
||||
</UserSettingField>
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
|
||||
<hr class="my-4 border-gray-100/40 dark:border-white/[0.03]" />
|
||||
|
||||
{#if $config?.features.enable_login_form && $config?.features.enable_password_change_form}
|
||||
<div class="mt-2">
|
||||
<UserSettingSection title={$i18n.t('Password')}>
|
||||
<UpdatePassword />
|
||||
</div>
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
|
||||
{#if ($config?.features?.enable_api_keys ?? true) && ($user?.role === 'admin' || ($user?.permissions?.features?.api_keys ?? false))}
|
||||
<div class="mt-3 flex items-center justify-between text-sm">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('API keys')}</div>
|
||||
<button
|
||||
class="text-xs font-normal text-gray-500 transition-colors hover:text-gray-700 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAPIKeys = !showAPIKeys;
|
||||
}}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
<UserSettingSection title={$i18n.t('API keys')}>
|
||||
<UserSettingRow description={$i18n.t('Show or hide sensitive account secrets.')}>
|
||||
<span slot="label">{$i18n.t('Secrets')}</span>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showAPIKeys = !showAPIKeys;
|
||||
}}>{showAPIKeys ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</UserSettingRow>
|
||||
|
||||
{#if showAPIKeys}
|
||||
<div class="flex flex-col">
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class="justify-between w-full mt-2">
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="mb-1 self-center text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('JWT Token')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<SensitiveInput value={localStorage.token} readOnly={true} />
|
||||
|
||||
<button
|
||||
class="ml-1.5 rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Copy Token')}
|
||||
on:click={() => {
|
||||
copyToClipboard(localStorage.token);
|
||||
JWTTokenCopied = true;
|
||||
setTimeout(() => {
|
||||
JWTTokenCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if JWTTokenCopied}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if ($config?.features?.enable_api_keys ?? true) && ($user?.role === 'admin' || ($user?.permissions?.features?.api_keys ?? false))}
|
||||
<div class="justify-between w-full mt-2">
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class="flex justify-between w-full">
|
||||
<div class="mb-1 self-center text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('API Key')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
{#if APIKey}
|
||||
<SensitiveInput value={APIKey} readOnly={true} />
|
||||
{#if showAPIKeys}
|
||||
<div class="flex flex-col gap-2.5">
|
||||
{#if $user?.role === 'admin'}
|
||||
<UserSettingField
|
||||
label={$i18n.t('JWT Token')}
|
||||
description={$i18n.t('Copy the current session token for authenticated requests.')}
|
||||
>
|
||||
<div class="flex">
|
||||
<SensitiveInput value={localStorage.token} readOnly={true} />
|
||||
|
||||
<button
|
||||
class="ml-1.5 rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Copy API Key')}
|
||||
aria-label={$i18n.t('Copy Token')}
|
||||
on:click={() => {
|
||||
copyToClipboard(APIKey);
|
||||
APIKeyCopied = true;
|
||||
copyToClipboard(localStorage.token);
|
||||
JWTTokenCopied = true;
|
||||
setTimeout(() => {
|
||||
APIKeyCopied = false;
|
||||
JWTTokenCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if APIKeyCopied}
|
||||
{#if JWTTokenCopied}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
|
|
@ -375,48 +309,106 @@
|
|||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</UserSettingField>
|
||||
{/if}
|
||||
|
||||
{#if ($config?.features?.enable_api_keys ?? true) && ($user?.role === 'admin' || ($user?.permissions?.features?.api_keys ?? false))}
|
||||
<UserSettingField
|
||||
label={$i18n.t('API Key')}
|
||||
description={$i18n.t('Create, copy, or rotate your API key.')}
|
||||
>
|
||||
<div class="flex">
|
||||
{#if APIKey}
|
||||
<SensitiveInput value={APIKey} readOnly={true} />
|
||||
|
||||
<Tooltip content={$i18n.t('Create new key')}>
|
||||
<button
|
||||
class="rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Create new key')}
|
||||
class="ml-1.5 rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Copy API Key')}
|
||||
on:click={() => {
|
||||
copyToClipboard(APIKey);
|
||||
APIKeyCopied = true;
|
||||
setTimeout(() => {
|
||||
APIKeyCopied = false;
|
||||
}, 2000);
|
||||
}}
|
||||
>
|
||||
{#if APIKeyCopied}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M16.704 4.153a.75.75 0 01.143 1.052l-8 10.5a.75.75 0 01-1.127.075l-4.5-4.5a.75.75 0 011.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 011.05-.143z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M11.986 3H12a2 2 0 0 1 2 2v6a2 2 0 0 1-1.5 1.937V7A2.5 2.5 0 0 0 10 4.5H4.063A2 2 0 0 1 6 3h.014A2.25 2.25 0 0 1 8.25 1h1.5a2.25 2.25 0 0 1 2.236 2ZM10.5 4v-.75a.75.75 0 0 0-.75-.75h-1.5a.75.75 0 0 0-.75.75V4h3Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M3 6a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h7a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H3Zm1.75 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5ZM4 11.75a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<Tooltip content={$i18n.t('Create new key')}>
|
||||
<button
|
||||
class="rounded-sm px-1.5 py-1 text-gray-500 transition hover:bg-gray-100 hover:text-gray-700 dark:hover:bg-gray-850 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Create new key')}
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{:else}
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-[0.8125rem] font-normal text-gray-600 transition-colors hover:text-gray-900 dark:text-gray-400 dark:hover:text-white"
|
||||
on:click={() => {
|
||||
createAPIKeyHandler();
|
||||
}}
|
||||
>
|
||||
<Plus strokeWidth="2" className=" size-3.5" />
|
||||
<Plus strokeWidth="2" className=" size-3.5" />
|
||||
|
||||
{$i18n.t('Create new secret key')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{$i18n.t('Create new secret key')}</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</UserSettingField>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,10 @@
|
|||
let currentPassword = '';
|
||||
let newPassword = '';
|
||||
let newPasswordConfirm = '';
|
||||
const inputClass =
|
||||
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const actionButtonClass =
|
||||
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
const updatePasswordHandler = async () => {
|
||||
if (newPassword === newPasswordConfirm) {
|
||||
|
|
@ -43,25 +47,30 @@
|
|||
updatePasswordHandler();
|
||||
}}
|
||||
>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">{$i18n.t('Change Password')}</div>
|
||||
<div class="flex items-center justify-between gap-2.5">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Change Password')}</div>
|
||||
<button
|
||||
class="text-xs font-normal text-gray-500 transition-colors hover:text-gray-700 dark:hover:text-gray-300"
|
||||
class={actionButtonClass}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
show = !show;
|
||||
}}>{show ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
<p class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Update the password used for email and password sign-in.')}
|
||||
</p>
|
||||
|
||||
{#if show}
|
||||
<div class=" py-2.5 space-y-1.5">
|
||||
<div class="py-2.5 space-y-2.5">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Current Password')}</div>
|
||||
<div class="mb-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('Current Password')}
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<SensitiveInput
|
||||
class="w-full bg-transparent text-sm dark:text-gray-300 outline-hidden placeholder:opacity-30"
|
||||
class={inputClass}
|
||||
type="password"
|
||||
bind:value={currentPassword}
|
||||
placeholder={$i18n.t('Enter your current password')}
|
||||
|
|
@ -72,11 +81,13 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('New Password')}</div>
|
||||
<div class="mb-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('New Password')}
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<SensitiveInput
|
||||
class="w-full bg-transparent text-sm dark:text-gray-300 outline-hidden placeholder:opacity-30"
|
||||
class={inputClass}
|
||||
type="password"
|
||||
bind:value={newPassword}
|
||||
placeholder={$i18n.t('Enter your new password')}
|
||||
|
|
@ -87,11 +98,13 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Confirm Password')}</div>
|
||||
<div class="mb-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('Confirm Password')}
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<SensitiveInput
|
||||
class="w-full bg-transparent text-sm dark:text-gray-300 outline-hidden placeholder:opacity-30"
|
||||
class={inputClass}
|
||||
type="password"
|
||||
bind:value={newPasswordConfirm}
|
||||
placeholder={$i18n.t('Confirm your new password')}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@
|
|||
/>
|
||||
|
||||
{#if variant === 'account'}
|
||||
<div class="mb-6 flex items-center gap-4">
|
||||
<div class="mb-2 flex items-center gap-4">
|
||||
<button
|
||||
class="group relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-full bg-gray-100
|
||||
ring-1 ring-gray-200 transition-all duration-200 hover:ring-2 hover:ring-gray-300 dark:bg-white/8 dark:ring-white/10 dark:hover:ring-white/20"
|
||||
|
|
@ -122,12 +122,10 @@
|
|||
</button>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white"
|
||||
>{displayName || user?.name}</span
|
||||
>
|
||||
<span class="text-xs text-gray-600 dark:text-gray-400">{displayName || user?.name}</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
class="text-[0.6875rem] text-gray-500 transition-colors duration-100 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
profileImageInputElement.click();
|
||||
|
|
@ -135,7 +133,7 @@
|
|||
>
|
||||
<span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
class="text-[0.6875rem] text-gray-500 transition-colors duration-100 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
profileImageUrl = `${WEBUI_BASE_URL}/user.png`;
|
||||
|
|
@ -143,7 +141,7 @@
|
|||
>
|
||||
<span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
class="text-[0.6875rem] text-gray-500 transition-colors duration-100 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (canvasPixelTest()) {
|
||||
|
|
@ -162,7 +160,7 @@
|
|||
>
|
||||
<span class="text-[0.6875rem] text-gray-300 dark:text-gray-700">·</span>
|
||||
<button
|
||||
class="text-[0.6875rem] text-gray-400 transition-colors duration-100 hover:text-gray-600 dark:hover:text-gray-300"
|
||||
class="text-[0.6875rem] text-gray-500 transition-colors duration-100 hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
const url = await getGravatarUrl(localStorage.token, user?.email);
|
||||
|
|
@ -215,7 +213,7 @@
|
|||
>
|
||||
|
||||
<button
|
||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
class="rounded-lg py-0.5 text-center text-xs text-gray-600 opacity-0 transition-all group-hover:opacity-100 dark:text-gray-400"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
if (canvasPixelTest()) {
|
||||
|
|
@ -234,7 +232,7 @@
|
|||
>
|
||||
|
||||
<button
|
||||
class=" text-xs text-center text-gray-800 dark:text-gray-400 rounded-lg py-0.5 opacity-0 group-hover:opacity-100 transition-all"
|
||||
class="rounded-lg py-0.5 text-center text-xs text-gray-600 opacity-0 transition-all group-hover:opacity-100 dark:text-gray-400"
|
||||
type="button"
|
||||
on:click={async () => {
|
||||
const url = await getGravatarUrl(localStorage.token, user?.email);
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@
|
|||
|
||||
<div
|
||||
class={layout === 'grid'
|
||||
? 'grid grid-cols-1 gap-x-5 gap-y-1 pb-safe-bottom text-xs sm:grid-cols-2 lg:grid-cols-3'
|
||||
: 'space-y-1 text-xs pb-safe-bottom'}
|
||||
? 'grid grid-cols-1 gap-x-5 gap-y-1 pb-safe-bottom text-xs text-gray-600 dark:text-gray-400 sm:grid-cols-2 lg:grid-cols-3'
|
||||
: 'space-y-1 pb-safe-bottom text-xs text-gray-600 dark:text-gray-400'}
|
||||
>
|
||||
<div>
|
||||
<Tooltip
|
||||
|
|
|
|||
|
|
@ -181,7 +181,7 @@
|
|||
/>
|
||||
|
||||
<div id="tab-archived-chats" class="flex flex-col h-full text-sm">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{$i18n.t('Archived Chats')}
|
||||
{#if chatCount !== null}
|
||||
|
|
@ -192,22 +192,25 @@
|
|||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex h-8 shrink-0 items-center w-full gap-2">
|
||||
<div class="flex min-w-0 flex-1 items-center">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<div class="flex h-7 shrink-0 items-center w-full gap-2">
|
||||
<div
|
||||
class="flex min-w-0 flex-1 items-center rounded-lg bg-gray-50/70 px-2 dark:bg-white/[0.03]"
|
||||
>
|
||||
<div class="mr-2 self-center text-gray-400 dark:text-gray-600">
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
data-settings-search
|
||||
class="w-full bg-transparent py-1 text-xs text-gray-700 outline-hidden placeholder:text-gray-300 dark:text-gray-300 dark:placeholder:text-gray-700"
|
||||
bind:value={query}
|
||||
on:input={searchHandler}
|
||||
placeholder={$i18n.t('Search')}
|
||||
maxlength="500"
|
||||
/>
|
||||
{#if query}
|
||||
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||
<div class="self-center pl-1.5">
|
||||
<button
|
||||
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
class="rounded-lg p-0.5 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Clear search')}
|
||||
on:click={() => {
|
||||
|
|
@ -224,7 +227,7 @@
|
|||
<Dropdown align="end">
|
||||
<Tooltip content={$i18n.t('Actions')}>
|
||||
<button
|
||||
class="flex h-8 items-center gap-1.5 rounded-xl bg-transparent px-1.5 text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
|
||||
class="flex h-7 items-center gap-1.5 rounded-lg bg-transparent px-1.5 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
|
||||
type="button"
|
||||
>
|
||||
<span>{$i18n.t('Actions')}</span>
|
||||
|
|
@ -235,7 +238,7 @@
|
|||
<div slot="content">
|
||||
<DropdownMenu className="w-[170px] shadow-sm">
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
disabled={loading || chatCount === 0}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
|
|
@ -251,7 +254,7 @@
|
|||
</button>
|
||||
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
disabled={loading || chatCount === 0}
|
||||
type="button"
|
||||
on:click={exportChatsHandler}
|
||||
|
|
@ -264,7 +267,7 @@
|
|||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover px-2 pr-1.5">
|
||||
<div class="mt-3 flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
{#if chatList === null}
|
||||
<div class="flex min-h-20 items-center justify-center">
|
||||
<Spinner className="size-5" />
|
||||
|
|
@ -276,7 +279,7 @@
|
|||
{$i18n.t('You have no archived conversations.')}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center text-xs text-gray-400 dark:text-gray-600">
|
||||
<div class="flex items-center px-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
<button
|
||||
class="flex flex-1 items-center gap-1 py-0.5 text-left"
|
||||
type="button"
|
||||
|
|
@ -309,7 +312,7 @@
|
|||
|
||||
<div>
|
||||
{#each chatList as chat (chat.id)}
|
||||
<div class="py-0.5 flex w-full justify-between gap-2 text-xs">
|
||||
<div class="flex w-full justify-between gap-2 px-1 py-1 text-xs">
|
||||
<a
|
||||
class="min-w-0 flex-1 self-center truncate text-gray-600 hover:text-black dark:text-gray-400 dark:hover:text-white"
|
||||
href={`/c/${chat.id}`}
|
||||
|
|
@ -338,7 +341,7 @@
|
|||
<div class="flex shrink-0 items-center justify-end text-gray-500 dark:text-gray-500">
|
||||
<Tooltip content={$i18n.t('Unarchive Chat')}>
|
||||
<button
|
||||
class="rounded-sm p-1 hover:bg-gray-100 dark:hover:bg-gray-850"
|
||||
class="rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Unarchive Chat')}
|
||||
on:click={() => {
|
||||
|
|
@ -350,7 +353,7 @@
|
|||
</Tooltip>
|
||||
<Tooltip content={$i18n.t('Delete Chat')}>
|
||||
<button
|
||||
class="rounded-sm p-1 hover:bg-gray-100 dark:hover:bg-gray-850"
|
||||
class="rounded-lg p-1 text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Delete Chat')}
|
||||
on:click={() => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,9 @@
|
|||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import UserSettingField from './UserSettingField.svelte';
|
||||
import UserSettingRow from './UserSettingRow.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
|
@ -35,6 +38,12 @@
|
|||
|
||||
// Audio speed control
|
||||
let playbackRate = 1;
|
||||
const inputClass =
|
||||
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const selectClass =
|
||||
'h-7 w-fit min-w-28 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-right text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
|
||||
const actionButtonClass =
|
||||
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
const getVoices = async () => {
|
||||
if (TTSEngine === 'browser-kokoro') {
|
||||
|
|
@ -154,7 +163,7 @@
|
|||
|
||||
<form
|
||||
id="tab-audio"
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
saveSettings({
|
||||
audio: {
|
||||
|
|
@ -177,57 +186,55 @@
|
|||
>
|
||||
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$i18n.t('Audio')}</h2>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 space-y-3">
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
|
||||
{$i18n.t('STT Settings')}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<UserSettingSection title={$i18n.t('STT Settings')} first>
|
||||
{#if $config.audio.stt.engine !== 'web'}
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Speech-to-Text Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={STTEngine}
|
||||
aria-label={$i18n.t('Speech-to-Text Engine')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="web">{$i18n.t('Web API')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Speech-to-Text Engine')}
|
||||
description={$i18n.t('Choose the engine used to transcribe voice input.')}
|
||||
>
|
||||
<select
|
||||
class={selectClass}
|
||||
bind:value={STTEngine}
|
||||
aria-label={$i18n.t('Speech-to-Text Engine')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="web">{$i18n.t('Web API')}</option>
|
||||
</select>
|
||||
</UserSettingRow>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Language')}</div>
|
||||
|
||||
<div class="flex items-center relative text-xs px-3">
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The language of the input audio. Supplying the input language in ISO-639-1 (e.g. en) format will improve accuracy and latency. Leave blank to automatically detect the language.'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={STTLanguage}
|
||||
aria-label={$i18n.t('Speech-to-Text Language')}
|
||||
placeholder={$i18n.t('e.g. en')}
|
||||
class=" text-sm text-right bg-transparent dark:text-gray-300 outline-hidden"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Language')}
|
||||
description={$i18n.t(
|
||||
'Set a speech recognition language or leave it blank to detect automatically.'
|
||||
)}
|
||||
>
|
||||
<Tooltip
|
||||
content={$i18n.t(
|
||||
'The language of the input audio. Supplying the input language in ISO-639-1 (e.g. en) format will improve accuracy and latency. Leave blank to automatically detect the language.'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={STTLanguage}
|
||||
aria-label={$i18n.t('Speech-to-Text Language')}
|
||||
placeholder={$i18n.t('e.g. en')}
|
||||
class="h-7 w-24 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-right text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500"
|
||||
/>
|
||||
</Tooltip>
|
||||
</UserSettingRow>
|
||||
{/if}
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">
|
||||
{$i18n.t('Instant Auto-Send After Voice Transcription')}
|
||||
</div>
|
||||
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Instant Auto-Send After Voice Transcription')}
|
||||
description={$i18n.t(
|
||||
'Send transcribed voice input immediately after speech recognition finishes.'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
toggleSpeechAutoSend();
|
||||
}}
|
||||
|
|
@ -235,60 +242,53 @@
|
|||
role="switch"
|
||||
aria-checked={speechAutoSend}
|
||||
>
|
||||
{#if speechAutoSend === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
{speechAutoSend === true ? $i18n.t('On') : $i18n.t('Off')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</UserSettingRow>
|
||||
</UserSettingSection>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
|
||||
{$i18n.t('TTS Settings')}
|
||||
</div>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Text-to-Speech Engine')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={TTSEngine}
|
||||
aria-label={$i18n.t('Text-to-Speech Engine')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingSection title={$i18n.t('TTS Settings')}>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Text-to-Speech Engine')}
|
||||
description={$i18n.t('Choose the engine used to read assistant responses aloud.')}
|
||||
>
|
||||
<select
|
||||
class={selectClass}
|
||||
bind:value={TTSEngine}
|
||||
aria-label={$i18n.t('Text-to-Speech Engine')}
|
||||
placeholder={$i18n.t('Select an engine')}
|
||||
>
|
||||
<option value="">{$i18n.t('Default')}</option>
|
||||
<option value="browser-kokoro">{$i18n.t('Kokoro.js (Browser)')}</option>
|
||||
</select>
|
||||
</UserSettingRow>
|
||||
|
||||
{#if TTSEngine === 'browser-kokoro'}
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Kokoro.js Dtype')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class="w-fit pr-8 rounded-sm px-2 p-1 text-xs bg-transparent outline-hidden text-right"
|
||||
bind:value={TTSEngineConfig.dtype}
|
||||
aria-label={$i18n.t('Kokoro.js Dtype')}
|
||||
placeholder={$i18n.t('Select dtype')}
|
||||
>
|
||||
<option value="" disabled selected>{$i18n.t('Select dtype')}</option>
|
||||
<option value="fp32">fp32</option>
|
||||
<option value="fp16">fp16</option>
|
||||
<option value="q8">q8</option>
|
||||
<option value="q4">q4</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Kokoro.js Dtype')}
|
||||
description={$i18n.t('Select the local model precision used by Kokoro.js.')}
|
||||
>
|
||||
<select
|
||||
class={selectClass}
|
||||
bind:value={TTSEngineConfig.dtype}
|
||||
aria-label={$i18n.t('Kokoro.js Dtype')}
|
||||
placeholder={$i18n.t('Select dtype')}
|
||||
>
|
||||
<option value="" disabled selected>{$i18n.t('Select dtype')}</option>
|
||||
<option value="fp32">fp32</option>
|
||||
<option value="fp16">fp16</option>
|
||||
<option value="q8">q8</option>
|
||||
<option value="q4">q4</option>
|
||||
</select>
|
||||
</UserSettingRow>
|
||||
{/if}
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Auto-Playback Response')}</div>
|
||||
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Auto-Playback Response')}
|
||||
description={$i18n.t('Play assistant responses aloud automatically.')}
|
||||
>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
toggleResponseAutoPlayback();
|
||||
}}
|
||||
|
|
@ -296,114 +296,38 @@
|
|||
role="switch"
|
||||
aria-checked={responseAutoPlayback}
|
||||
>
|
||||
{#if responseAutoPlayback === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
{responseAutoPlayback === true ? $i18n.t('On') : $i18n.t('Off')}
|
||||
</button>
|
||||
</div>
|
||||
</UserSettingRow>
|
||||
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Speech Playback Speed')}</div>
|
||||
|
||||
<div class="flex items-center relative text-xs px-3">
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Speech Playback Speed')}
|
||||
description={$i18n.t('Adjust how quickly spoken responses are played.')}
|
||||
>
|
||||
<div class="relative flex items-center gap-1.5 text-xs text-gray-400 dark:text-gray-600">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
bind:value={playbackRate}
|
||||
aria-label={$i18n.t('Speech Playback Speed')}
|
||||
class=" text-sm text-right bg-transparent dark:text-gray-300 outline-hidden"
|
||||
class="h-7 w-16 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-right text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500"
|
||||
/>
|
||||
x
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100/20 dark:border-white/[0.025]" />
|
||||
</UserSettingRow>
|
||||
</UserSettingSection>
|
||||
|
||||
{#if TTSEngine === 'browser-kokoro'}
|
||||
{#if TTSModel}
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2.5">
|
||||
{$i18n.t('Set Voice')}
|
||||
</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden"
|
||||
bind:value={voice}
|
||||
aria-label={$i18n.t('Voice')}
|
||||
placeholder={$i18n.t('Select a voice')}
|
||||
/>
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-normal flex gap-2 items-center">
|
||||
<Spinner className="size-4" />
|
||||
|
||||
<div class=" text-sm font-normal shimmer">
|
||||
{$i18n.t('Loading Kokoro.js...')}
|
||||
{TTSModelProgress && TTSModelProgress.status === 'progress'
|
||||
? `(${Math.round(TTSModelProgress.progress * 10) / 10}%)`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500">
|
||||
{$i18n.t('Please do not close the settings page while loading the model.')}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if $config.audio.tts.engine === ''}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-normal">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<select
|
||||
class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden"
|
||||
bind:value={voice}
|
||||
aria-label={$i18n.t('Voice')}
|
||||
>
|
||||
<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
|
||||
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
|
||||
<option
|
||||
value={_voice.name}
|
||||
class="bg-gray-100 dark:bg-gray-700"
|
||||
selected={voice === _voice.name}>{_voice.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between my-1.5">
|
||||
<div class="text-xs">
|
||||
{$i18n.t('Allow non-local voices')}
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch bind:state={nonLocalVoices} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if $config.audio.tts.engine !== ''}
|
||||
<div>
|
||||
<div class=" mb-2.5 text-sm font-normal">{$i18n.t('Set Voice')}</div>
|
||||
<div class="flex w-full">
|
||||
<div class="flex-1">
|
||||
<UserSettingSection title={$i18n.t('Voice')}>
|
||||
<UserSettingField
|
||||
label={$i18n.t('Set Voice')}
|
||||
description={$i18n.t('Choose the Kokoro.js voice used for speech output.')}
|
||||
>
|
||||
<input
|
||||
list="voice-list"
|
||||
class="w-full text-sm bg-transparent dark:text-gray-300 outline-hidden"
|
||||
class={inputClass}
|
||||
bind:value={voice}
|
||||
aria-label={$i18n.t('Voice')}
|
||||
placeholder={$i18n.t('Select a voice')}
|
||||
|
|
@ -414,9 +338,71 @@
|
|||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</UserSettingField>
|
||||
</UserSettingSection>
|
||||
{:else}
|
||||
<UserSettingSection title={$i18n.t('Voice')}>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<Spinner className="size-4" />
|
||||
|
||||
<div class="shimmer">
|
||||
{$i18n.t('Loading Kokoro.js...')}
|
||||
{TTSModelProgress && TTSModelProgress.status === 'progress'
|
||||
? `(${Math.round(TTSModelProgress.progress * 10) / 10}%)`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Please do not close the settings page while loading the model.')}
|
||||
</div>
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
{:else if $config.audio.tts.engine === ''}
|
||||
<UserSettingSection title={$i18n.t('Voice')}>
|
||||
<UserSettingField
|
||||
label={$i18n.t('Set Voice')}
|
||||
description={$i18n.t('Choose the browser voice used for speech output.')}
|
||||
>
|
||||
<select class={inputClass} bind:value={voice} aria-label={$i18n.t('Voice')}>
|
||||
<option value="" selected={voice !== ''}>{$i18n.t('Default')}</option>
|
||||
{#each voices.filter((v) => nonLocalVoices || v.localService === true) as _voice}
|
||||
<option
|
||||
value={_voice.name}
|
||||
class="bg-gray-100 dark:bg-gray-700"
|
||||
selected={voice === _voice.name}>{_voice.name}</option
|
||||
>
|
||||
{/each}
|
||||
</select>
|
||||
</UserSettingField>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Allow non-local voices')}
|
||||
description={$i18n.t('Include voices that are not provided by a local speech service.')}
|
||||
>
|
||||
<Switch bind:state={nonLocalVoices} />
|
||||
</UserSettingRow>
|
||||
</UserSettingSection>
|
||||
{:else if $config.audio.tts.engine !== ''}
|
||||
<UserSettingSection title={$i18n.t('Voice')}>
|
||||
<UserSettingField
|
||||
label={$i18n.t('Set Voice')}
|
||||
description={$i18n.t('Choose the configured text-to-speech service voice.')}
|
||||
>
|
||||
<input
|
||||
list="voice-list"
|
||||
class={inputClass}
|
||||
bind:value={voice}
|
||||
aria-label={$i18n.t('Voice')}
|
||||
placeholder={$i18n.t('Select a voice')}
|
||||
/>
|
||||
|
||||
<datalist id="voice-list">
|
||||
{#each voices as voice}
|
||||
<option value={voice.id}>{voice.name}</option>
|
||||
{/each}
|
||||
</datalist>
|
||||
</UserSettingField>
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { models, settings, user } from '$lib/stores';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Connection from './Connections/Connection.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
|
||||
|
|
@ -80,72 +77,59 @@
|
|||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
{#if config !== null}
|
||||
<div class="">
|
||||
<div class="pr-1.5">
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Manage Direct Connections')}
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
aria-label={$i18n.t('Add Connection')}
|
||||
on:click={() => {
|
||||
showConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx}
|
||||
<Connection
|
||||
bind:url
|
||||
bind:key={config.OPENAI_API_KEYS[idx]}
|
||||
bind:config={config.OPENAI_API_CONFIGS[idx]}
|
||||
onSubmit={() => {
|
||||
updateHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter(
|
||||
(key, keyIdx) => idx !== keyIdx
|
||||
);
|
||||
|
||||
let newConfig = {};
|
||||
config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] =
|
||||
config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
config.OPENAI_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<UserSettingSection title={$i18n.t('Manage Direct Connections')} first>
|
||||
<div class="flex items-center justify-between gap-2.5">
|
||||
<div class="min-w-0 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
|
||||
</div>
|
||||
|
||||
<div class="my-1.5">
|
||||
<div
|
||||
class="text-xs {($settings?.highContrastMode ?? false)
|
||||
? 'text-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-500'}"
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
aria-label={$i18n.t('Add Connection')}
|
||||
on:click={() => {
|
||||
showConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
|
||||
<br />
|
||||
{$i18n.t(
|
||||
'CORS must be properly configured by the provider to allow requests from Open WebUI.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each config?.OPENAI_API_BASE_URLS ?? [] as url, idx}
|
||||
<Connection
|
||||
bind:url
|
||||
bind:key={config.OPENAI_API_KEYS[idx]}
|
||||
bind:config={config.OPENAI_API_CONFIGS[idx]}
|
||||
onSubmit={() => {
|
||||
updateHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
config.OPENAI_API_BASE_URLS = config.OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
config.OPENAI_API_KEYS = config.OPENAI_API_KEYS.filter(
|
||||
(key, keyIdx) => idx !== keyIdx
|
||||
);
|
||||
|
||||
let newConfig = {};
|
||||
config.OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] = config.OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
config.OPENAI_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t(
|
||||
'CORS must be properly configured by the provider to allow requests from Open WebUI.'
|
||||
)}
|
||||
</div>
|
||||
</UserSettingSection>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { getContext, tick } from 'svelte';
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Cog6 from '$lib/components/icons/Cog6.svelte';
|
||||
import AddConnectionModal from '$lib/components/AddConnectionModal.svelte';
|
||||
|
||||
|
|
@ -19,9 +17,6 @@
|
|||
export let config = {};
|
||||
|
||||
let showConfigModal = false;
|
||||
|
||||
const highContrastInputClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
</script>
|
||||
|
||||
<AddConnectionModal
|
||||
|
|
@ -45,7 +40,7 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<div class="flex w-full gap-2 items-center">
|
||||
<div class="flex w-full items-center gap-3">
|
||||
<Tooltip
|
||||
className="w-full relative"
|
||||
content={$i18n.t(`WebUI will make requests to "{{url}}/chat/completions"`, {
|
||||
|
|
@ -61,7 +56,7 @@
|
|||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1 relative">
|
||||
<input
|
||||
class={`w-full ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent outline-hidden'} ${pipeline ? 'pr-8' : ''}`}
|
||||
class={`h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500 ${pipeline ? 'pr-8' : ''}`}
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={url}
|
||||
autocomplete="off"
|
||||
|
|
@ -70,11 +65,11 @@
|
|||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<div class="flex shrink-0 items-center 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:hover:bg-gray-850 rounded-lg transition"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={() => {
|
||||
showConfigModal = true;
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@
|
|||
import SharedChatsModal from '$lib/components/layout/SharedChatsModal.svelte';
|
||||
import FilesModal from '$lib/components/layout/FilesModal.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import UserSettingRow from './UserSettingRow.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
|
|
@ -27,6 +29,8 @@
|
|||
let showFilesModal = false;
|
||||
|
||||
let chatImportInputElement: HTMLInputElement;
|
||||
const actionButtonClass =
|
||||
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
$: if (importFiles) {
|
||||
console.log(importFiles);
|
||||
|
|
@ -137,7 +141,7 @@
|
|||
{$i18n.t('Data Controls')}
|
||||
</h2>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 space-y-3">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<input
|
||||
id="chat-import-input"
|
||||
bind:this={chatImportInputElement}
|
||||
|
|
@ -147,106 +151,102 @@
|
|||
hidden
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">{$i18n.t('Chats')}</div>
|
||||
|
||||
<UserSettingSection title={$i18n.t('Chats')} first>
|
||||
{#if $user?.role === 'admin' || ($user.permissions?.chat?.import ?? true)}
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Import Chats')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
chatImportInputElement.click();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Import')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Import Chats')}
|
||||
description={$i18n.t('Import chat history from a JSON export file.')}
|
||||
>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
chatImportInputElement.click();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Import')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Export Chats')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
exportChats();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Export')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Export Chats')}
|
||||
description={$i18n.t('Download your chat history as a JSON export.')}
|
||||
>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
exportChats();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Export')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Shared Chats')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
showSharedChatsModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Manage')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Shared Chats')}
|
||||
description={$i18n.t('Review and manage chats you have shared.')}
|
||||
>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
showSharedChatsModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Manage')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Archive All Chats')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
showArchiveConfirmDialog = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Archive All')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Archive All Chats')}
|
||||
description={$i18n.t('Move every chat into the archive after confirmation.')}
|
||||
>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
showArchiveConfirmDialog = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Archive All')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Delete All Chats')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Delete All')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Delete All Chats')}
|
||||
description={$i18n.t('Permanently delete every chat after confirmation.')}
|
||||
>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
showDeleteConfirmDialog = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Delete All')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
</UserSettingSection>
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">{$i18n.t('Files')}</div>
|
||||
|
||||
<div>
|
||||
<div class="py-0.5 flex w-full justify-between">
|
||||
<div class="self-center text-xs">{$i18n.t('Manage Files')}</div>
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
showFilesModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span class="self-center">{$i18n.t('Manage')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingSection title={$i18n.t('Files')}>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Manage Files')}
|
||||
description={$i18n.t('Open the file manager for uploaded files.')}
|
||||
>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
showFilesModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Manage')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
</UserSettingSection>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
|
||||
import AdvancedParams from './Advanced/AdvancedParams.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import UserSettingField from './UserSettingField.svelte';
|
||||
import UserSettingRow from './UserSettingRow.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
export let saveSettings: Function;
|
||||
export let getModels: Function;
|
||||
|
||||
|
|
@ -25,11 +28,11 @@
|
|||
let showAdvanced = false;
|
||||
|
||||
const systemPromptTextareaClass =
|
||||
'w-full resize-vertical rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const compactSystemPromptTextareaClass =
|
||||
'w-full resize-vertical appearance-none bg-transparent text-sm outline-hidden focus:bg-transparent disabled:bg-transparent dark:text-gray-300';
|
||||
const highContrastSelectClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
|
||||
'w-full resize-y rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const selectClass =
|
||||
'h-7 w-fit min-w-28 rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-right text-xs text-gray-700 outline-hidden transition-colors focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:focus:border-blue-500';
|
||||
const actionButtonClass =
|
||||
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
const toggleNotification = async () => {
|
||||
const permission = await Notification.requestPermission();
|
||||
|
|
@ -216,59 +219,49 @@
|
|||
<h2 class="text-sm font-medium text-gray-900 dark:text-white mb-4">{$i18n.t('General')}</h2>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<div class="">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 mb-2">
|
||||
{$i18n.t('WebUI Settings')}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Theme')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class={`w-fit pr-8 text-xs text-right ${$settings.highContrastMode ? highContrastSelectClass : 'rounded-sm bg-transparent px-2 py-2 outline-hidden'}`}
|
||||
bind:value={selectedTheme}
|
||||
placeholder={$i18n.t('Select a theme')}
|
||||
on:change={() => themeChangeHandler(selectedTheme)}
|
||||
>
|
||||
<option value="system">⚙️ {$i18n.t('System')}</option>
|
||||
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
||||
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
|
||||
<option value="light">☀️ {$i18n.t('Light')}</option>
|
||||
{#if $config?.features?.enable_easter_eggs}
|
||||
<option value="her">🌷 Her</option>
|
||||
{/if}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Language')}</div>
|
||||
<div class="flex items-center relative">
|
||||
<select
|
||||
class={`w-fit pr-8 text-xs text-right ${$settings.highContrastMode ? highContrastSelectClass : 'rounded-sm bg-transparent px-2 py-2 outline-hidden'}`}
|
||||
bind:value={lang}
|
||||
placeholder={$i18n.t('Select a language')}
|
||||
on:change={(e) => {
|
||||
changeLanguage(lang);
|
||||
}}
|
||||
>
|
||||
{#each languages as language}
|
||||
<option value={language['code']}>{language['title']}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{#if $i18n.language === 'en-US' && !($config?.license_metadata ?? false)}
|
||||
<div
|
||||
class="mb-2 text-xs {($settings?.highContrastMode ?? false)
|
||||
? 'text-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-400 dark:text-gray-500'}"
|
||||
<UserSettingSection title={$i18n.t('WebUI Settings')} first>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Theme')}
|
||||
description={$i18n.t('Choose the color theme used by the interface.')}
|
||||
>
|
||||
<select
|
||||
class={selectClass}
|
||||
bind:value={selectedTheme}
|
||||
placeholder={$i18n.t('Select a theme')}
|
||||
on:change={() => themeChangeHandler(selectedTheme)}
|
||||
>
|
||||
<option value="system">⚙️ {$i18n.t('System')}</option>
|
||||
<option value="dark">🌑 {$i18n.t('Dark')}</option>
|
||||
<option value="oled-dark">🌃 {$i18n.t('OLED Dark')}</option>
|
||||
<option value="light">☀️ {$i18n.t('Light')}</option>
|
||||
{#if $config?.features?.enable_easter_eggs}
|
||||
<option value="her">🌷 Her</option>
|
||||
{/if}
|
||||
</select>
|
||||
</UserSettingRow>
|
||||
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Language')}
|
||||
description={$i18n.t('Choose the language used for interface text.')}
|
||||
>
|
||||
<select
|
||||
class={selectClass}
|
||||
bind:value={lang}
|
||||
placeholder={$i18n.t('Select a language')}
|
||||
on:change={(e) => {
|
||||
changeLanguage(lang);
|
||||
}}
|
||||
>
|
||||
{#each languages as language}
|
||||
<option value={language['code']}>{language['title']}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</UserSettingRow>
|
||||
{#if $i18n.language === 'en-US' && !($config?.license_metadata ?? false)}
|
||||
<div class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
Couldn't find your language?
|
||||
<a
|
||||
class="font-normal underline {($settings?.highContrastMode ?? false)
|
||||
? 'text-gray-700 dark:text-gray-200'
|
||||
: 'text-gray-300'}"
|
||||
class="font-normal underline text-gray-400 dark:text-gray-600"
|
||||
href="https://github.com/open-webui/open-webui/blob/main/docs/CONTRIBUTING.md#-translations-and-internationalization"
|
||||
target="_blank"
|
||||
>
|
||||
|
|
@ -277,69 +270,55 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<div class=" py-0.5 flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-normal">{$i18n.t('Notifications')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded-sm transition"
|
||||
on:click={() => {
|
||||
toggleNotification();
|
||||
}}
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={notificationEnabled}
|
||||
>
|
||||
{#if notificationEnabled === true}
|
||||
<span class="ml-2 self-center">{$i18n.t('On')}</span>
|
||||
{:else}
|
||||
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<UserSettingRow
|
||||
label={$i18n.t('Notifications')}
|
||||
description={$i18n.t('Allow browser notifications for completed responses.')}
|
||||
>
|
||||
<button
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
toggleNotification();
|
||||
}}
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={notificationEnabled}
|
||||
>
|
||||
{notificationEnabled === true ? $i18n.t('On') : $i18n.t('Off')}
|
||||
</button>
|
||||
</UserSettingRow>
|
||||
</UserSettingSection>
|
||||
|
||||
{#if $user?.role === 'admin' || (($user?.permissions.chat?.controls ?? true) && ($user?.permissions.chat?.system_prompt ?? true))}
|
||||
<hr class="border-gray-100/20 dark:border-white/[0.025] my-3" />
|
||||
|
||||
<div>
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600 my-2.5">
|
||||
{$i18n.t('System Prompt')}
|
||||
</div>
|
||||
<Textarea
|
||||
bind:value={system}
|
||||
className={($settings?.highContrastMode ?? false)
|
||||
? systemPromptTextareaClass
|
||||
: compactSystemPromptTextareaClass}
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt here')}
|
||||
/>
|
||||
</div>
|
||||
<UserSettingSection title={$i18n.t('System Prompt')}>
|
||||
<UserSettingField description={$i18n.t('Set the default system prompt for new chats.')}>
|
||||
<Textarea
|
||||
bind:value={system}
|
||||
className={systemPromptTextareaClass}
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt here')}
|
||||
/>
|
||||
</UserSettingField>
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
|
||||
{#if $user?.role === 'admin' || (($user?.permissions.chat?.controls ?? true) && ($user?.permissions.chat?.params ?? true))}
|
||||
<div class="mt-2 space-y-3 pr-1.5">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Advanced Parameters')}
|
||||
</div>
|
||||
<UserSettingSection title={$i18n.t('Advanced Parameters')}>
|
||||
<UserSettingRow description={$i18n.t('Show or hide custom generation parameters.')}>
|
||||
<span slot="label">{$i18n.t('Model parameters')}</span>
|
||||
<button
|
||||
class=" text-xs font-normal {($settings?.highContrastMode ?? false)
|
||||
? 'text-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-400 dark:text-gray-500'}"
|
||||
class={actionButtonClass}
|
||||
type="button"
|
||||
aria-expanded={showAdvanced}
|
||||
on:click={() => {
|
||||
showAdvanced = !showAdvanced;
|
||||
}}>{showAdvanced ? $i18n.t('Hide') : $i18n.t('Show')}</button
|
||||
>
|
||||
</div>
|
||||
</UserSettingRow>
|
||||
|
||||
{#if showAdvanced}
|
||||
<AdvancedParams admin={$user?.role === 'admin'} custom={true} bind:params />
|
||||
{/if}
|
||||
</div>
|
||||
</UserSettingSection>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { getToolServersData } from '$lib/apis';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
import { settings, toolServers, terminalServers } from '$lib/stores';
|
||||
|
||||
|
|
@ -13,17 +14,31 @@
|
|||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Connection from './Tools/Connection.svelte';
|
||||
import Terminals from './Integrations/Terminals.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
|
||||
import AddToolServerModal from '$lib/components/AddToolServerModal.svelte';
|
||||
|
||||
export let saveSettings: Function;
|
||||
type TerminalServerConfig = {
|
||||
url: string;
|
||||
key?: string;
|
||||
name?: string;
|
||||
enabled: boolean;
|
||||
auth_type?: string;
|
||||
path?: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
let servers = null;
|
||||
let terminalServerConfigs: { url: string; key: string; name?: string; enabled: boolean }[] = [];
|
||||
type ToolServerConnection = any;
|
||||
|
||||
export let saveSettings: (settings: any) => void | Promise<void>;
|
||||
|
||||
let servers: ToolServerConnection[] | null = null;
|
||||
let terminalServerConfigs: TerminalServerConfig[] = [];
|
||||
let showConnectionModal = false;
|
||||
const helpTextClass = 'text-[0.6875rem] text-gray-400 dark:text-gray-600';
|
||||
|
||||
const addConnectionHandler = async (server) => {
|
||||
servers = [...servers, server];
|
||||
const addConnectionHandler = async (server: ToolServerConnection) => {
|
||||
servers = [...(servers ?? []), server];
|
||||
await updateHandler();
|
||||
};
|
||||
|
||||
|
|
@ -34,7 +49,7 @@
|
|||
});
|
||||
|
||||
let toolServersData = await getToolServersData($settings?.toolServers ?? []);
|
||||
toolServersData = toolServersData.filter((data) => {
|
||||
toolServersData = toolServersData.filter((data: any) => {
|
||||
if (data.error) {
|
||||
toast.error(
|
||||
$i18n.t(`Failed to connect to {{URL}} OpenAPI tool server`, { URL: data?.url })
|
||||
|
|
@ -43,10 +58,10 @@
|
|||
}
|
||||
return true;
|
||||
});
|
||||
toolServers.set(toolServersData);
|
||||
toolServers.set(toolServersData as any);
|
||||
|
||||
// Refresh terminal servers store (preserve system terminals)
|
||||
const existingSystemTerminals = ($terminalServers ?? []).filter((t) => t.id);
|
||||
const existingSystemTerminals = (($terminalServers ?? []) as any[]).filter((t) => t.id);
|
||||
const activeTerminals = terminalServerConfigs.filter((s) => s.enabled);
|
||||
if (activeTerminals.length > 0) {
|
||||
let terminalServersData = await getToolServersData(
|
||||
|
|
@ -58,16 +73,16 @@
|
|||
config: { enable: true }
|
||||
}))
|
||||
);
|
||||
terminalServersData = terminalServersData.filter((data) => data && !data.error);
|
||||
terminalServers.set([...terminalServersData, ...existingSystemTerminals]);
|
||||
terminalServersData = terminalServersData.filter((data: any) => data && !data.error);
|
||||
terminalServers.set([...terminalServersData, ...existingSystemTerminals] as any);
|
||||
} else {
|
||||
terminalServers.set(existingSystemTerminals);
|
||||
terminalServers.set(existingSystemTerminals as any);
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
servers = $settings?.toolServers ?? [];
|
||||
terminalServerConfigs = $settings?.terminalServers ?? [];
|
||||
terminalServerConfigs = ($settings as any)?.terminalServers ?? [];
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
@ -84,86 +99,75 @@
|
|||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
{#if servers !== null}
|
||||
<div>
|
||||
<div class="pr-1.5">
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center mb-0.5">
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Manage Tool Servers')}
|
||||
</div>
|
||||
|
||||
<Tooltip content={$i18n.t('Add Connection')}>
|
||||
<button
|
||||
aria-label={$i18n.t('Add Connection')}
|
||||
class="px-1"
|
||||
on:click={() => (showConnectionModal = true)}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<UserSettingSection title={$i18n.t('Tools')} first>
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('External Tool Servers')}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5">
|
||||
{#each servers as server, idx}
|
||||
<Connection
|
||||
bind:connection={server}
|
||||
direct
|
||||
onSubmit={() => updateHandler()}
|
||||
onDelete={() => {
|
||||
servers = servers.filter((_, i) => i !== idx);
|
||||
updateHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
<Tooltip content={$i18n.t('Add Connection')}>
|
||||
<button
|
||||
aria-label={$i18n.t('Add Connection')}
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
on:click={() => (showConnectionModal = true)}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="my-1.5">
|
||||
<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(
|
||||
'CORS must be properly configured by the provider to allow requests from Open WebUI.'
|
||||
)}
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each servers as server, idx}
|
||||
<Connection
|
||||
bind:connection={server}
|
||||
direct
|
||||
onSubmit={() => updateHandler()}
|
||||
onDelete={() => {
|
||||
servers = (servers ?? []).filter((_, i) => i !== idx);
|
||||
updateHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300 mb-2">
|
||||
{#if (servers ?? []).length === 0}
|
||||
<div class={helpTextClass}>
|
||||
{$i18n.t('No tool server connections configured.')}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-1 {helpTextClass}">
|
||||
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
|
||||
</div>
|
||||
<div class={helpTextClass}>
|
||||
{$i18n.t(
|
||||
'CORS must be properly configured by the provider to allow requests from Open WebUI.'
|
||||
)}
|
||||
<a
|
||||
class="underline"
|
||||
class="ml-1 text-gray-500 underline hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
href="https://github.com/open-webui/openapi-servers"
|
||||
target="_blank">{$i18n.t('Learn more about OpenAPI tool servers.')} ↗</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</UserSettingSection>
|
||||
|
||||
<hr class="border-gray-100/50 dark:border-gray-850/50 my-4" />
|
||||
<UserSettingSection title={$i18n.t('Terminal')}>
|
||||
<Terminals bind:servers={terminalServerConfigs} onChange={() => updateHandler()} />
|
||||
|
||||
<div class="pr-1.5">
|
||||
<Terminals bind:servers={terminalServerConfigs} onChange={() => updateHandler()} />
|
||||
|
||||
<div class="mt-1.5">
|
||||
<div
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t(
|
||||
'Connect to Open Terminal instances to browse files and use them as always-on tools. Only one can be active at a time.'
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-600 dark:text-gray-300 mt-1">
|
||||
<a
|
||||
class="underline"
|
||||
href="https://github.com/open-webui/open-terminal"
|
||||
target="_blank">{$i18n.t('Learn more about Open Terminal')} ↗</a
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 {helpTextClass}">
|
||||
{$i18n.t(
|
||||
'Connect to Open Terminal instances to browse files and use them as always-on tools. Only one can be active at a time.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="mt-0.5 block text-[0.6875rem] text-gray-500 underline hover:text-gray-700 dark:text-gray-500 dark:hover:text-gray-300"
|
||||
href="https://github.com/open-webui/open-terminal"
|
||||
target="_blank">{$i18n.t('Learn more about Open Terminal')} ↗</a
|
||||
>
|
||||
</UserSettingSection>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
|
|
|
|||
|
|
@ -1,18 +1,30 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Connection from './Terminals/Connection.svelte';
|
||||
import AddTerminalServerModal from '$lib/components/AddTerminalServerModal.svelte';
|
||||
|
||||
export let servers = [];
|
||||
export let onChange: (servers: typeof servers) => void = () => {};
|
||||
type TerminalServerConfig = {
|
||||
url: string;
|
||||
key?: string;
|
||||
name?: string;
|
||||
path?: string;
|
||||
enabled: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export let servers: TerminalServerConfig[] = [];
|
||||
export let onChange: (servers: TerminalServerConfig[]) => void = () => {};
|
||||
|
||||
let showAddModal = false;
|
||||
|
||||
const addServer = (server: (typeof servers)[0]) => {
|
||||
const addServer = (server: TerminalServerConfig) => {
|
||||
servers = [...servers, server];
|
||||
onChange(servers);
|
||||
};
|
||||
|
|
@ -27,7 +39,7 @@
|
|||
onChange(servers);
|
||||
};
|
||||
|
||||
const updateServer = (idx: number, updated: (typeof servers)[0]) => {
|
||||
const updateServer = (idx: number, updated: TerminalServerConfig) => {
|
||||
servers = servers.map((s, i) => (i === idx ? updated : s));
|
||||
onChange(servers);
|
||||
};
|
||||
|
|
@ -38,16 +50,20 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<AddTerminalServerModal direct bind:show={showAddModal} onSubmit={(server) => addServer(server)} />
|
||||
<AddTerminalServerModal
|
||||
direct
|
||||
bind:show={showAddModal}
|
||||
onSubmit={(server: TerminalServerConfig) => addServer(server)}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="font-normal">{$i18n.t('Open Terminal')}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Open Terminal')}</div>
|
||||
</div>
|
||||
<Tooltip content={$i18n.t('Add Connection')}>
|
||||
<button
|
||||
class="px-1"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:bg-black/5 hover:text-gray-900 dark:text-gray-600 dark:hover:bg-white/5 dark:hover:text-white"
|
||||
on:click={() => (showAddModal = true)}
|
||||
type="button"
|
||||
aria-label={$i18n.t('Add Connection')}
|
||||
|
|
@ -70,16 +86,8 @@
|
|||
</div>
|
||||
|
||||
{#if servers.length === 0}
|
||||
<div class="text-xs text-gray-400 dark:text-gray-500">
|
||||
<div class="text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('No terminal connections configured.')}
|
||||
<a
|
||||
href="https://github.com/open-webui/open-terminal"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="underline hover:text-gray-700 dark:hover:text-gray-200"
|
||||
>
|
||||
{$i18n.t('Learn more')} ↗
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -8,8 +11,23 @@
|
|||
import AddTerminalServerModal from '$lib/components/AddTerminalServerModal.svelte';
|
||||
import Cloud from '$lib/components/icons/Cloud.svelte';
|
||||
|
||||
export let connection = { url: '', key: '', name: '', path: '/openapi.json', enabled: false };
|
||||
export let onSubmit: (c: typeof connection) => void = () => {};
|
||||
type TerminalServerConfig = {
|
||||
url: string;
|
||||
key?: string;
|
||||
name?: string;
|
||||
path?: string;
|
||||
enabled: boolean;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export let connection: TerminalServerConfig = {
|
||||
url: '',
|
||||
key: '',
|
||||
name: '',
|
||||
path: '/openapi.json',
|
||||
enabled: false
|
||||
};
|
||||
export let onSubmit: (c: TerminalServerConfig) => void = () => {};
|
||||
export let onDelete: () => void = () => {};
|
||||
export let onEnable: () => void = () => {};
|
||||
export let onDisable: () => void = () => {};
|
||||
|
|
@ -26,7 +44,7 @@
|
|||
onDelete();
|
||||
showConfigModal = false;
|
||||
}}
|
||||
onSubmit={(c) => {
|
||||
onSubmit={(c: TerminalServerConfig) => {
|
||||
connection = c;
|
||||
onSubmit(c);
|
||||
}}
|
||||
|
|
@ -42,7 +60,7 @@
|
|||
<Cloud className="size-4" strokeWidth="1.5" />
|
||||
</Tooltip>
|
||||
|
||||
<div class="outline-hidden w-full bg-transparent text-sm">
|
||||
<div class="outline-hidden w-full bg-transparent text-xs text-gray-700 dark:text-gray-300">
|
||||
{connection.name || connection.url || $i18n.t('New Terminal')}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -52,7 +70,7 @@
|
|||
<div class="flex gap-1 items-center">
|
||||
<Tooltip content={$i18n.t('Configure')}>
|
||||
<button
|
||||
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
class="self-center p-1 bg-transparent hover:bg-black/5 dark:hover:bg-white/5 rounded-lg transition"
|
||||
on:click={() => {
|
||||
showConfigModal = true;
|
||||
}}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -10,6 +10,7 @@
|
|||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
export let show = false;
|
||||
export let onSave = () => {};
|
||||
|
|
@ -27,9 +28,9 @@
|
|||
onMount(() => {});
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<Modal size="sm" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<div class=" flex justify-between text-gray-900 dark:text-white px-4 pt-3 pb-1">
|
||||
<h1 class="text-sm font-medium self-center">
|
||||
{$i18n.t('Quick Actions')}
|
||||
</h1>
|
||||
|
|
@ -44,7 +45,9 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div
|
||||
class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full px-1"
|
||||
|
|
@ -57,7 +60,7 @@
|
|||
<div class="text-xs flex items-center justify-between mb-2">
|
||||
<div class="font-normal">{$i18n.t('Actions')}</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-gray-700 dark:text-gray-300">
|
||||
<div class="flex items-center gap-2 text-gray-500 dark:text-gray-500">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => {
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
export let show = false;
|
||||
export let size = null;
|
||||
|
|
@ -23,9 +24,9 @@
|
|||
onMount(() => {});
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
<Modal size="sm" bind:show className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-4 pt-3 pb-1">
|
||||
<div class=" flex justify-between text-gray-900 dark:text-white px-4 pt-3 pb-1">
|
||||
<h1 class="text-sm font-medium self-center">
|
||||
{$i18n.t('Manage')}
|
||||
</h1>
|
||||
|
|
@ -40,7 +41,9 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div
|
||||
class="flex flex-col md:flex-row w-full px-4 pb-4 md:space-x-4 text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form
|
||||
class="flex flex-col w-full px-1"
|
||||
|
|
@ -52,7 +55,10 @@
|
|||
<div>
|
||||
<div>
|
||||
<div class=" py-0.5 flex flex-col w-full text-sm">
|
||||
<div id="image-compression-size-label" class=" text-xs mb-2">
|
||||
<div
|
||||
id="image-compression-size-label"
|
||||
class="mb-2 text-xs text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{$i18n.t('Image Max Compression Size')}
|
||||
</div>
|
||||
|
||||
|
|
@ -65,7 +71,7 @@
|
|||
id="image-comp-width"
|
||||
bind:value={size.width}
|
||||
type="number"
|
||||
class="w-full bg-transparent outline-hidden text-center"
|
||||
class="h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-center text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500"
|
||||
min="0"
|
||||
placeholder={$i18n.t('Width')}
|
||||
/>
|
||||
|
|
@ -83,7 +89,7 @@
|
|||
id="image-comp-height"
|
||||
bind:value={size.height}
|
||||
type="number"
|
||||
class="w-full bg-transparent outline-hidden text-center"
|
||||
class="h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-center text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500"
|
||||
min="0"
|
||||
placeholder={$i18n.t('Height')}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,18 +2,27 @@
|
|||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { config, settings } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||
import MemoryModal from './Personalization/MemoryModal.svelte';
|
||||
import { deleteMemoriesByUserId, deleteMemoryById, getMemories } from '$lib/apis/memories';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import UserSettingRow from './UserSettingRow.svelte';
|
||||
import UserSettingSection from './UserSettingSection.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Trash from '$lib/components/icons/Trash.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
export let saveSettings: Function;
|
||||
export let saveSettings: (settings: any) => void | Promise<void>;
|
||||
|
||||
// Addons
|
||||
let enableMemory = false;
|
||||
|
|
@ -25,6 +34,10 @@
|
|||
let showClearConfirmDialog = false;
|
||||
let showDeleteConfirm = false;
|
||||
let query = '';
|
||||
const inputClass =
|
||||
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const actionButtonClass =
|
||||
'shrink-0 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
type Memory = {
|
||||
id: string;
|
||||
|
|
@ -90,7 +103,7 @@
|
|||
|
||||
<form
|
||||
id="tab-personalization"
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
class="flex flex-col h-full justify-between text-sm"
|
||||
on:submit|preventDefault={() => {
|
||||
dispatch('save');
|
||||
}}
|
||||
|
|
@ -99,148 +112,158 @@
|
|||
{$i18n.t('Personalization')}
|
||||
</h2>
|
||||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5 py-1">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<UserSettingSection title={$i18n.t('Memory')} first>
|
||||
<UserSettingRow
|
||||
description={$i18n
|
||||
.t(
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you."
|
||||
)
|
||||
.replace($i18n.t('Manage'), $i18n.t('Add Memory'))}
|
||||
>
|
||||
<Tooltip
|
||||
slot="label"
|
||||
content={$i18n.t(
|
||||
'This is an experimental feature, it may not function as expected and is subject to change at any time.'
|
||||
)}
|
||||
>
|
||||
<div class="flex items-center gap-2 text-xs text-gray-400 dark:text-gray-600">
|
||||
<div class="flex items-center gap-2">
|
||||
{$i18n.t('Memory')}
|
||||
<span
|
||||
class="inline-flex items-center text-[0.625rem] font-normal uppercase leading-none text-gray-400 dark:text-gray-600"
|
||||
<span class="text-[0.625rem] uppercase text-gray-400 dark:text-gray-600"
|
||||
>{$i18n.t('Experimental')}</span
|
||||
>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={enableMemory}
|
||||
on:change={async () => {
|
||||
saveSettings({ memory: enableMemory });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n
|
||||
.t(
|
||||
"You can personalize your interactions with LLMs by adding memories through the 'Manage' button below, making them more helpful and tailored to you."
|
||||
)
|
||||
.replace($i18n.t('Manage'), $i18n.t('Add Memory'))}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
bind:state={enableMemory}
|
||||
on:change={async () => {
|
||||
saveSettings({ memory: enableMemory });
|
||||
}}
|
||||
/>
|
||||
</UserSettingRow>
|
||||
|
||||
{#if enableMemory}
|
||||
<div class="mt-5">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Memory')}
|
||||
</h3>
|
||||
|
||||
{#if !loadingMemories}
|
||||
<div class="text-xs text-gray-400 dark:text-gray-600">{memories.length}</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||
on:click={() => {
|
||||
selectedMemory = null;
|
||||
showMemoryModal = true;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Add Memory')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if loadingMemories}
|
||||
<div class="w-full flex justify-center items-center min-h-16">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{:else}
|
||||
{#if memories.length > 0}
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<input
|
||||
class="w-full text-xs py-1 bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search Memories')}
|
||||
maxlength="500"
|
||||
/>
|
||||
|
||||
{#if query}
|
||||
<button
|
||||
type="button"
|
||||
class="shrink-0 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
on:click={() => {
|
||||
query = '';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Clear')}
|
||||
</button>
|
||||
{#if enableMemory}
|
||||
<div>
|
||||
<div class="mb-2 flex items-center justify-between">
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('Saved Memories')}
|
||||
{#if !loadingMemories}
|
||||
<span class="ml-1 text-gray-400 dark:text-gray-600">{memories.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if sortedMemories.length === 0}
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400 min-h-16 flex items-center">
|
||||
{#if memories.length === 0}
|
||||
{$i18n.t('Memories accessible by LLMs will be shown here.')}
|
||||
{:else}
|
||||
{$i18n.t('No results found')}
|
||||
{/if}
|
||||
<Dropdown align="end">
|
||||
<Tooltip content={$i18n.t('Actions')}>
|
||||
<button
|
||||
class="flex h-7 items-center gap-1.5 rounded-lg bg-transparent px-1.5 text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
|
||||
type="button"
|
||||
>
|
||||
<span>{$i18n.t('Actions')}</span>
|
||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="w-[170px] shadow-sm">
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
disabled={loadingMemories}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
selectedMemory = null;
|
||||
showMemoryModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3.5 shrink-0" strokeWidth="1.5" />
|
||||
<div class="min-w-0 flex-1 truncate text-left">{$i18n.t('Add Memory')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="flex h-[1.6875rem] w-full cursor-pointer select-none items-center gap-2 rounded-lg bg-transparent px-2 text-xs hover:text-gray-900 disabled:cursor-default disabled:opacity-30 dark:hover:text-gray-100"
|
||||
disabled={loadingMemories || memories.length === 0}
|
||||
type="button"
|
||||
on:click={() => {
|
||||
showClearConfirmDialog = true;
|
||||
}}
|
||||
>
|
||||
<Trash className="size-3.5 shrink-0" strokeWidth="1.5" />
|
||||
<div class="min-w-0 flex-1 truncate text-left">{$i18n.t('Clear memory')}</div>
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
{#if loadingMemories}
|
||||
<div class="flex min-h-16 w-full items-center justify-center">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2.5">
|
||||
{#each sortedMemories as memory (memory.id)}
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div
|
||||
class="min-w-0 whitespace-pre-wrap break-words text-xs text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{memory.content}
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
on:click={() => editMemory(memory)}
|
||||
>
|
||||
{$i18n.t('Edit')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
|
||||
on:click={() => confirmDeleteMemory(memory)}
|
||||
>
|
||||
{$i18n.t('Remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if memories.length > 0}
|
||||
<div class="mb-2 flex items-center gap-2">
|
||||
<input
|
||||
class={inputClass}
|
||||
bind:value={query}
|
||||
placeholder={$i18n.t('Search Memories')}
|
||||
maxlength="500"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="mt-3 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 hover:underline transition"
|
||||
on:click={() => {
|
||||
if (memories.length > 0) {
|
||||
showClearConfirmDialog = true;
|
||||
} else {
|
||||
toast.error($i18n.t('No memories to clear'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Clear memory')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if query}
|
||||
<button
|
||||
type="button"
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
query = '';
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Clear')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if sortedMemories.length === 0}
|
||||
<div class="min-h-16 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{#if memories.length === 0}
|
||||
{$i18n.t('Memories accessible by LLMs will be shown here.')}
|
||||
{:else}
|
||||
{$i18n.t('No results found')}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each sortedMemories as memory (memory.id)}
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div
|
||||
class="min-w-0 whitespace-pre-wrap break-words text-xs text-gray-700 dark:text-gray-300"
|
||||
>
|
||||
{memory.content}
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="{actionButtonClass} hover:underline"
|
||||
on:click={() => editMemory(memory)}
|
||||
>
|
||||
{$i18n.t('Edit')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="{actionButtonClass} hover:underline"
|
||||
on:click={() => confirmDeleteMemory(memory)}
|
||||
>
|
||||
{$i18n.t('Remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</UserSettingSection>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0 flex justify-end text-sm font-normal">
|
||||
|
|
@ -287,7 +310,7 @@
|
|||
<div class="text-sm text-gray-500 flex-1">
|
||||
{$i18n.t('Are you sure you want to delete this memory? This action cannot be undone.')}
|
||||
<div
|
||||
class="mt-2 bg-gray-50 dark:bg-gray-900 p-3 rounded-xl border border-gray-100 dark:border-gray-800 text-black dark:text-white whitespace-pre-wrap break-words max-h-32 overflow-y-auto"
|
||||
class="mt-2 max-h-32 overflow-y-auto whitespace-pre-wrap break-words rounded-lg border border-gray-100/50 bg-gray-50/40 p-2 text-xs text-gray-600 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-400"
|
||||
>
|
||||
{selectedMemory?.content}
|
||||
</div>
|
||||
|
|
@ -296,7 +319,7 @@
|
|||
|
||||
<MemoryModal
|
||||
bind:show={showMemoryModal}
|
||||
memory={selectedMemory}
|
||||
memory={selectedMemory as any}
|
||||
on:save={async () => {
|
||||
await loadMemories();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
|
@ -19,6 +20,12 @@
|
|||
let content = '';
|
||||
let type = 'user';
|
||||
let path = '';
|
||||
const inputClass =
|
||||
'h-7 w-full rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const textareaClass =
|
||||
'w-full resize-y rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-xs text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
const actionButtonClass =
|
||||
'text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white';
|
||||
|
||||
$: edit = !!memory?.id;
|
||||
$: if (show) {
|
||||
|
|
@ -51,9 +58,9 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="sm">
|
||||
<Modal bind:show size="sm" className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-300 px-4 pt-3 pb-1">
|
||||
<div class=" flex justify-between text-gray-900 dark:text-white px-4 pt-3 pb-1">
|
||||
<div class=" text-sm font-medium self-center">
|
||||
{#if edit}
|
||||
{$i18n.t('Edit Memory')}
|
||||
|
|
@ -69,16 +76,18 @@
|
|||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
|
||||
<div
|
||||
class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
<form class="flex flex-col w-full" on:submit|preventDefault={submitHandler}>
|
||||
<div class="px-1">
|
||||
<div class="flex w-full justify-between items-center mb-1.5">
|
||||
<div class="text-xs text-gray-500">{$i18n.t('Type')}</div>
|
||||
<div class="text-xs text-gray-600 dark:text-gray-400">{$i18n.t('Type')}</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-gray-700 dark:text-gray-300"
|
||||
class={actionButtonClass}
|
||||
on:click={() => {
|
||||
type = type === 'user' ? 'context' : 'user';
|
||||
}}
|
||||
|
|
@ -93,16 +102,15 @@
|
|||
|
||||
<textarea
|
||||
bind:value={content}
|
||||
class="bg-transparent w-full text-sm outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"
|
||||
class={textareaClass}
|
||||
rows="6"
|
||||
style="resize: vertical;"
|
||||
placeholder={type === 'user'
|
||||
? $i18n.t('Add a preference, fact, or instruction about you')
|
||||
: $i18n.t('Add durable context for future chats')}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col w-full mt-1.5">
|
||||
<label for="memory-path" class="mb-0.5 text-xs text-gray-500">
|
||||
<label for="memory-path" class="mb-1 text-xs text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('Path')}
|
||||
<span class="opacity-50">({$i18n.t('optional')})</span>
|
||||
</label>
|
||||
|
|
@ -110,7 +118,7 @@
|
|||
<input
|
||||
id="memory-path"
|
||||
bind:value={path}
|
||||
class="w-full text-sm bg-transparent outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700"
|
||||
class={inputClass}
|
||||
placeholder={$i18n.t('Path')}
|
||||
autocomplete="off"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@
|
|||
</h2>
|
||||
|
||||
<button
|
||||
class="text-[0.625rem] text-gray-400 transition hover:text-gray-600 dark:hover:text-gray-300"
|
||||
class="text-xs text-gray-500 transition-colors hover:text-gray-900 dark:text-gray-500 dark:hover:text-white"
|
||||
on:click={resetBindings}
|
||||
>
|
||||
{$i18n.t('Reset Defaults')}
|
||||
|
|
@ -114,21 +114,17 @@
|
|||
|
||||
<div class="flex-1 min-h-0 overflow-y-auto scrollbar-hover pr-1.5">
|
||||
<div class="flex items-center gap-2 px-1 pb-1">
|
||||
<span class="flex-1 text-[0.625rem] font-medium text-gray-400 dark:text-gray-600">
|
||||
<span class="flex-1 text-[0.625rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Command')}
|
||||
</span>
|
||||
<span
|
||||
class="w-[9.5rem] shrink-0 text-right text-[0.625rem] font-medium text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<span class="w-[9.5rem] shrink-0 text-right text-[0.625rem] text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t('Key')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#each Object.entries(categorizedShortcuts) as [category, items], categoryIndex}
|
||||
<div class={categoryIndex > 0 ? 'mt-3' : ''}>
|
||||
<div
|
||||
class="px-1 pb-0.5 pt-1 text-[0.625rem] font-medium uppercase tracking-wide text-gray-400 dark:text-gray-600"
|
||||
>
|
||||
<div class="px-1 pb-0.5 pt-1 text-xs text-gray-400 dark:text-gray-600">
|
||||
{$i18n.t(category)}
|
||||
</div>
|
||||
<div class="divide-y divide-gray-100/70 dark:divide-white/[0.03]">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
import { exportChatStats, exportSingleChatStats, downloadChatStats } from '$lib/apis/chats';
|
||||
import { getVersion } from '$lib/apis';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
|
@ -314,7 +315,7 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
<Modal bind:show size="md">
|
||||
<Modal bind:show size="md" className="bg-white dark:bg-gray-900 rounded-4xl">
|
||||
<div class="w-full">
|
||||
{#if completed}
|
||||
<div class="px-5.5 py-5">
|
||||
|
|
@ -380,7 +381,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-3 text-xs text-gray-500">
|
||||
<div class="font-normal text-gray-900 dark:text-gray-100 mb-1">
|
||||
<div class="mb-1 font-normal text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('What is shared:')}
|
||||
</div>
|
||||
<ul class="list-disc list-inside space-y-0.5 ml-1 mb-2">
|
||||
|
|
@ -391,7 +392,7 @@
|
|||
<li>{$i18n.t('User ratings (thumbs up/down)')}</li>
|
||||
</ul>
|
||||
|
||||
<div class="font-normal text-gray-900 dark:text-gray-100 mb-1">
|
||||
<div class="mb-1 font-normal text-gray-600 dark:text-gray-400">
|
||||
{$i18n.t('What is NOT shared:')}
|
||||
</div>
|
||||
<ul class="list-disc list-inside space-y-0.5 ml-1">
|
||||
|
|
@ -452,7 +453,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-5 flex justify-between items-center gap-2">
|
||||
<div class="mt-4 flex items-center justify-between gap-2">
|
||||
<div class="text-xs text-gray-400 text-center mr-auto">
|
||||
<button
|
||||
class="hover:underline px-2"
|
||||
|
|
@ -465,7 +466,7 @@
|
|||
</div>
|
||||
|
||||
<button
|
||||
class="px-4 py-2 rounded-full text-sm font-normal bg-gray-100 hover:bg-gray-200 dark:bg-gray-800 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100 transition disabled:cursor-not-allowed"
|
||||
class="rounded-full bg-gray-100 px-4 py-2 text-sm font-normal text-gray-600 transition hover:bg-gray-200 disabled:cursor-not-allowed dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-gray-700"
|
||||
on:click={() => {
|
||||
if (syncing) {
|
||||
cancelOperation();
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
<div class="flex w-full gap-2 items-center">
|
||||
<div class="flex w-full items-center gap-3 text-xs text-gray-600 dark:text-gray-400">
|
||||
<Tooltip className="w-full relative" content={''} placement="top-start">
|
||||
<div class="flex w-full">
|
||||
<div
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
</Tooltip>
|
||||
|
||||
{#if connection?.info?.name}
|
||||
<div class=" capitalize outline-hidden w-full bg-transparent">
|
||||
<div class="w-full bg-transparent capitalize outline-hidden">
|
||||
{connection?.info?.name ?? connection?.url}
|
||||
<span class="text-gray-500">{connection?.info?.id ?? ''}</span>
|
||||
</div>
|
||||
|
|
@ -59,10 +59,10 @@
|
|||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<div class="flex gap-1 items-center">
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
<Tooltip content={$i18n.t('Configure')} className="self-start">
|
||||
<button
|
||||
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition-colors hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-300"
|
||||
on:click={() => {
|
||||
showConfigModal = true;
|
||||
}}
|
||||
|
|
|
|||
24
src/lib/components/chat/Settings/UserSettingField.svelte
Normal file
24
src/lib/components/chat/Settings/UserSettingField.svelte
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
export let label = '';
|
||||
export let description = '';
|
||||
export let forId = '';
|
||||
export let className = '';
|
||||
</script>
|
||||
|
||||
<div class={className}>
|
||||
{#if label}
|
||||
<label class="text-xs text-gray-600 dark:text-gray-400" for={forId || undefined}>
|
||||
{label}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
<div class={label ? 'mt-1' : ''}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if description}
|
||||
<p class="mt-0.5 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
22
src/lib/components/chat/Settings/UserSettingRow.svelte
Normal file
22
src/lib/components/chat/Settings/UserSettingRow.svelte
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
export let label = '';
|
||||
export let description = '';
|
||||
export let className = '';
|
||||
export let labelClassName = '';
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between gap-2.5 {className}">
|
||||
<div class="min-w-0 text-xs text-gray-600 dark:text-gray-400 {labelClassName}">
|
||||
<slot name="label">{label}</slot>
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if description}
|
||||
<p class="-mt-1 text-[0.6875rem] text-gray-400 dark:text-gray-600">
|
||||
{description}
|
||||
</p>
|
||||
{/if}
|
||||
17
src/lib/components/chat/Settings/UserSettingSection.svelte
Normal file
17
src/lib/components/chat/Settings/UserSettingSection.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
export let title = '';
|
||||
export let first = false;
|
||||
export let className = '';
|
||||
</script>
|
||||
|
||||
<section class="w-full {first ? '' : 'mt-4'} {className}">
|
||||
{#if title}
|
||||
<h3 class="mb-2 text-xs text-gray-400 dark:text-gray-600">
|
||||
{title}
|
||||
</h3>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2.5">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -52,9 +52,6 @@
|
|||
|
||||
const i18n: Writable<any> = getContext('i18n');
|
||||
|
||||
const highContrastInputClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
|
||||
export let show: boolean | string | SettingsModalRequest = false;
|
||||
let modalShow = false;
|
||||
let lastShow: boolean | string | SettingsModalRequest = false;
|
||||
|
|
@ -847,14 +844,12 @@
|
|||
class="hidden md:flex items-center gap-1.5 h-7 px-2 mx-1 mt-1 mb-0.5 shrink-0 rounded-lg text-xs bg-gray-50/70 dark:bg-white/[0.03]"
|
||||
>
|
||||
<div class="self-center rounded-l-xl bg-transparent">
|
||||
<Search
|
||||
className="size-3.5"
|
||||
strokeWidth={($settings?.highContrastMode ?? false) ? '3' : '1.5'}
|
||||
/>
|
||||
<Search className="size-3.5" strokeWidth="1.5" />
|
||||
</div>
|
||||
<label class="sr-only" for="search-input-settings-modal">{$i18n.t('Search')}</label>
|
||||
<input
|
||||
class={`w-full text-xs ${($settings?.highContrastMode ?? false) ? highContrastInputClass : 'bg-transparent py-1 outline-hidden dark:text-gray-300'}`}
|
||||
data-settings-search
|
||||
class="w-full text-xs bg-transparent py-1 outline-hidden dark:text-gray-300"
|
||||
bind:value={search}
|
||||
id="search-input-settings-modal"
|
||||
on:input={searchDebounceHandler}
|
||||
|
|
@ -863,7 +858,7 @@
|
|||
</div>
|
||||
|
||||
<div
|
||||
class="tabs flex min-w-0 flex-1 min-h-0 overflow-x-auto md:overflow-x-hidden md:overflow-y-auto md:flex-col p-1 pl-0 md:pl-1 gap-px"
|
||||
class="tabs scrollbar-none flex min-w-0 flex-1 min-h-0 overflow-x-auto md:overflow-x-hidden md:overflow-y-auto md:flex-col p-1 pl-0 md:pl-1 gap-px"
|
||||
>
|
||||
<span
|
||||
class="hidden md:block text-[0.625rem] text-gray-400 dark:text-gray-600 px-2 mt-1.5 mb-0.5"
|
||||
|
|
@ -1191,26 +1186,3 @@
|
|||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
/* display: none; <- Crashes Chrome on hover */
|
||||
-webkit-appearance: none;
|
||||
margin: 0; /* <-- Apparently some margin are still there even though it's hidden */
|
||||
}
|
||||
|
||||
.tabs::-webkit-scrollbar {
|
||||
display: none; /* for Chrome, Safari and Opera */
|
||||
}
|
||||
|
||||
.tabs {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
appearance: textfield;
|
||||
-moz-appearance: textfield; /* Firefox */
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<script lang="ts">
|
||||
const i18n = getContext('i18n');
|
||||
import { getContext } from 'svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
export let id = 'password-input';
|
||||
export let value: string = '';
|
||||
export let placeholder = '';
|
||||
|
|
@ -17,7 +16,7 @@
|
|||
|
||||
let show = false;
|
||||
|
||||
const highContrastInputClass =
|
||||
const inputChromeClass =
|
||||
'rounded-lg border border-gray-100/50 bg-gray-50/40 px-2 py-1.5 text-gray-700 outline-hidden transition-colors placeholder:text-gray-300 focus:border-blue-400 dark:border-white/[0.04] dark:bg-white/[0.03] dark:text-gray-300 dark:placeholder:text-gray-700 dark:focus:border-blue-500';
|
||||
</script>
|
||||
|
||||
|
|
@ -27,7 +26,7 @@
|
|||
{/if}
|
||||
<input
|
||||
{id}
|
||||
class={`${inputClassName} ${show ? '' : 'password'} ${($settings?.highContrastMode ?? false) ? highContrastInputClass : ' outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-600'}`}
|
||||
class={`${inputClassName} ${show ? '' : 'password'} ${inputChromeClass}`}
|
||||
{placeholder}
|
||||
type={type === 'password' && !show ? 'password' : 'text'}
|
||||
bind:value
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@
|
|||
import { refreshChatList } from '$lib/stores/chat-list';
|
||||
import Messages from '../chat/Messages.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import PencilSquare from '../icons/PencilSquare.svelte';
|
||||
import PageEdit from '../icons/PageEdit.svelte';
|
||||
import EditPencilIcon from './Sidebar/icons/EditPencil.svelte';
|
||||
import NotesIcon from './Sidebar/icons/Notes.svelte';
|
||||
|
||||
import ChatMenu from './Sidebar/ChatMenu.svelte';
|
||||
import ShareChatModal from '../chat/ShareChatModal.svelte';
|
||||
|
|
@ -251,7 +251,7 @@
|
|||
show = false;
|
||||
onClose();
|
||||
},
|
||||
icon: PencilSquare
|
||||
icon: EditPencilIcon
|
||||
}
|
||||
];
|
||||
|
||||
|
|
@ -508,7 +508,7 @@
|
|||
show = false;
|
||||
onClose();
|
||||
},
|
||||
icon: PageEdit
|
||||
icon: NotesIcon
|
||||
}
|
||||
]
|
||||
: [])
|
||||
|
|
|
|||
|
|
@ -355,7 +355,10 @@ Based on the user's instruction, update and enhance the existing notes or select
|
|||
position="top"
|
||||
className="inline-block"
|
||||
>
|
||||
<span class="text-gray-500 text-sm">({$i18n.t('Experimental')})</span>
|
||||
<span
|
||||
class="inline-flex items-center text-[0.625rem] font-normal uppercase leading-none text-gray-400 dark:text-gray-600"
|
||||
>{$i18n.t('Experimental')}</span
|
||||
>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue