Merge branch 'websearch' into feat/backend-web-search
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<script lang="ts">
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { banners as _banners } from '$lib/stores';
|
||||
import type { Banner } from '$lib/types';
|
||||
|
||||
import { getBanners, setBanners } from '$lib/apis/configs';
|
||||
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
const i18n: Writable<i18nType> = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let banners: Banner[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
banners = await getBanners(localStorage.token);
|
||||
});
|
||||
|
||||
const updateBanners = async () => {
|
||||
_banners.set(await setBanners(localStorage.token, banners));
|
||||
};
|
||||
</script>
|
||||
|
||||
<form
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
updateBanners();
|
||||
saveHandler();
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80 h-full">
|
||||
<div class=" space-y-3 pr-1.5">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm font-semibold">
|
||||
{$i18n.t('Banners')}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
if (banners.length === 0 || banners.at(-1).content !== '') {
|
||||
banners = [
|
||||
...banners,
|
||||
{
|
||||
id: uuidv4(),
|
||||
type: '',
|
||||
title: '',
|
||||
content: '',
|
||||
dismissible: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
];
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M10.75 4.75a.75.75 0 00-1.5 0v4.5h-4.5a.75.75 0 000 1.5h4.5v4.5a.75.75 0 001.5 0v-4.5h4.5a.75.75 0 000-1.5h-4.5v-4.5z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex flex-col space-y-1">
|
||||
{#each banners as banner, bannerIdx}
|
||||
<div class=" flex justify-between">
|
||||
<div class="flex flex-row flex-1 border rounded-xl dark:border-gray-800">
|
||||
<select
|
||||
class="w-fit capitalize rounded-xl py-2 px-4 text-xs bg-transparent outline-none"
|
||||
bind:value={banner.type}
|
||||
>
|
||||
{#if banner.type == ''}
|
||||
<option value="" selected disabled class="text-gray-900">{$i18n.t('Type')}</option
|
||||
>
|
||||
{/if}
|
||||
<option value="info" class="text-gray-900">{$i18n.t('Info')}</option>
|
||||
<option value="warning" class="text-gray-900">{$i18n.t('Warning')}</option>
|
||||
<option value="error" class="text-gray-900">{$i18n.t('Error')}</option>
|
||||
<option value="success" class="text-gray-900">{$i18n.t('Success')}</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
class="pr-5 py-1.5 text-xs w-full bg-transparent outline-none"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
/>
|
||||
|
||||
<div class="relative top-1.5 -left-2">
|
||||
<Tooltip content="Dismissible" className="flex h-fit items-center">
|
||||
<Switch bind:state={banner.dismissible} />
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="px-2"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
banners.splice(bannerIdx, 1);
|
||||
banners = banners;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
<button
|
||||
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -34,7 +34,7 @@
|
||||
<div>
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('Database')}</div>
|
||||
|
||||
{#if $config?.enable_admin_export ?? true}
|
||||
{#if $config?.features.enable_admin_export ?? true}
|
||||
<div class=" flex w-full justify-between">
|
||||
<!-- <div class=" self-center text-xs font-medium">{$i18n.t('Allow Chat Deletion')}</div> -->
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { getWebhookUrl, updateWebhookUrl } from '$lib/apis';
|
||||
import {
|
||||
getCommunitySharingEnabledStatus,
|
||||
getWebhookUrl,
|
||||
toggleCommunitySharingEnabledStatus,
|
||||
updateWebhookUrl
|
||||
} from '$lib/apis';
|
||||
import {
|
||||
getDefaultUserRole,
|
||||
getJWTExpiresDuration,
|
||||
@@ -18,6 +23,7 @@
|
||||
let JWTExpiresIn = '';
|
||||
|
||||
let webhookUrl = '';
|
||||
let communitySharingEnabled = true;
|
||||
|
||||
const toggleSignUpEnabled = async () => {
|
||||
signUpEnabled = await toggleSignUpEnabledStatus(localStorage.token);
|
||||
@@ -35,11 +41,28 @@
|
||||
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
|
||||
};
|
||||
|
||||
const toggleCommunitySharingEnabled = async () => {
|
||||
communitySharingEnabled = await toggleCommunitySharingEnabledStatus(localStorage.token);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
||||
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
||||
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
||||
webhookUrl = await getWebhookUrl(localStorage.token);
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
signUpEnabled = await getSignUpEnabledStatus(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
defaultUserRole = await getDefaultUserRole(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
JWTExpiresIn = await getJWTExpiresDuration(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
webhookUrl = await getWebhookUrl(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
communitySharingEnabled = await getCommunitySharingEnabledStatus(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -114,6 +137,47 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" flex w-full justify-between">
|
||||
<div class=" self-center text-xs font-medium">{$i18n.t('Enable Community Sharing')}</div>
|
||||
|
||||
<button
|
||||
class="p-1 px-3 text-xs flex rounded transition"
|
||||
on:click={() => {
|
||||
toggleCommunitySharingEnabled();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if communitySharingEnabled}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M11.5 1A3.5 3.5 0 0 0 8 4.5V7H2.5A1.5 1.5 0 0 0 1 8.5v5A1.5 1.5 0 0 0 2.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 9.5 7V4.5a2 2 0 1 1 4 0v1.75a.75.75 0 0 0 1.5 0V4.5A3.5 3.5 0 0 0 11.5 1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="ml-2 self-center">{$i18n.t('Enabled')}</span>
|
||||
{: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="M8 1a3.5 3.5 0 0 0-3.5 3.5V7A1.5 1.5 0 0 0 3 8.5v5A1.5 1.5 0 0 0 4.5 15h7a1.5 1.5 0 0 0 1.5-1.5v-5A1.5 1.5 0 0 0 11.5 7V4.5A3.5 3.5 0 0 0 8 1Zm2 6V4.5a2 2 0 1 0-4 0V7h4Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span class="ml-2 self-center">{$i18n.t('Disabled')}</span>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700 my-3" />
|
||||
|
||||
<div class=" w-full justify-between">
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
|
||||
import { getBackendConfig, getModelFilterConfig, updateModelFilterConfig } from '$lib/apis';
|
||||
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
|
||||
import { getUserPermissions, updateUserPermissions } from '$lib/apis/users';
|
||||
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { models } from '$lib/stores';
|
||||
import { models, config } from '$lib/stores';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import { setDefaultModels } from '$lib/apis/configs';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let saveHandler: Function;
|
||||
|
||||
let defaultModelId = '';
|
||||
|
||||
let whitelistEnabled = false;
|
||||
let whitelistModels = [''];
|
||||
let permissions = {
|
||||
@@ -24,9 +28,10 @@
|
||||
const res = await getModelFilterConfig(localStorage.token);
|
||||
if (res) {
|
||||
whitelistEnabled = res.enabled;
|
||||
|
||||
whitelistModels = res.models.length > 0 ? res.models : [''];
|
||||
}
|
||||
|
||||
defaultModelId = $config.default_models ? $config?.default_models.split(',')[0] : '';
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -34,10 +39,13 @@
|
||||
class="flex flex-col h-full justify-between space-y-3 text-sm"
|
||||
on:submit|preventDefault={async () => {
|
||||
// console.log('submit');
|
||||
await updateUserPermissions(localStorage.token, permissions);
|
||||
|
||||
await setDefaultModels(localStorage.token, defaultModelId);
|
||||
await updateUserPermissions(localStorage.token, permissions);
|
||||
await updateModelFilterConfig(localStorage.token, whitelistEnabled, whitelistModels);
|
||||
saveHandler();
|
||||
|
||||
await config.set(await getBackendConfig());
|
||||
}}
|
||||
>
|
||||
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-80">
|
||||
@@ -88,26 +96,40 @@
|
||||
|
||||
<hr class=" dark:border-gray-700 my-2" />
|
||||
|
||||
<div class="mt-2 space-y-3 pr-1.5">
|
||||
<div class="mt-2 space-y-3">
|
||||
<div>
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-sm font-medium">{$i18n.t('Manage Models')}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=" space-y-1 mb-3">
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-xs font-medium">{$i18n.t('Default Model')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" space-y-3">
|
||||
<div>
|
||||
<div class="flex-1 mr-2">
|
||||
<select
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
bind:value={defaultModelId}
|
||||
placeholder="Select a model"
|
||||
>
|
||||
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
|
||||
{#each $models.filter((model) => model.id) as model}
|
||||
<option value={model.id} class="bg-gray-100 dark:bg-gray-700">{model.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" space-y-1">
|
||||
<div class="mb-2">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<div class=" text-xs font-medium">{$i18n.t('Model Whitelisting')}</div>
|
||||
|
||||
<button
|
||||
class=" text-xs font-medium text-gray-500"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
whitelistEnabled = !whitelistEnabled;
|
||||
}}>{whitelistEnabled ? $i18n.t('On') : $i18n.t('Off')}</button
|
||||
>
|
||||
<Switch bind:state={whitelistEnabled} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
import General from './Settings/General.svelte';
|
||||
import Users from './Settings/Users.svelte';
|
||||
|
||||
import Banners from '$lib/components/admin/Settings/Banners.svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -117,24 +120,63 @@
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Database')}</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
|
||||
'banners'
|
||||
? 'bg-gray-200 dark:bg-gray-700'
|
||||
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
|
||||
on:click={() => {
|
||||
selectedTab = 'banners';
|
||||
}}
|
||||
>
|
||||
<div class=" self-center mr-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
d="M5.85 3.5a.75.75 0 0 0-1.117-1 9.719 9.719 0 0 0-2.348 4.876.75.75 0 0 0 1.479.248A8.219 8.219 0 0 1 5.85 3.5ZM19.267 2.5a.75.75 0 1 0-1.118 1 8.22 8.22 0 0 1 1.987 4.124.75.75 0 0 0 1.48-.248A9.72 9.72 0 0 0 19.266 2.5Z"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12 2.25A6.75 6.75 0 0 0 5.25 9v.75a8.217 8.217 0 0 1-2.119 5.52.75.75 0 0 0 .298 1.206c1.544.57 3.16.99 4.831 1.243a3.75 3.75 0 1 0 7.48 0 24.583 24.583 0 0 0 4.83-1.244.75.75 0 0 0 .298-1.205 8.217 8.217 0 0 1-2.118-5.52V9A6.75 6.75 0 0 0 12 2.25ZM9.75 18c0-.034 0-.067.002-.1a25.05 25.05 0 0 0 4.496 0l.002.1a2.25 2.25 0 1 1-4.5 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class=" self-center">{$i18n.t('Banners')}</div>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex-1 md:min-h-[380px]">
|
||||
{#if selectedTab === 'general'}
|
||||
<General
|
||||
saveHandler={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'users'}
|
||||
<Users
|
||||
saveHandler={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'db'}
|
||||
<Database
|
||||
saveHandler={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{:else if selectedTab === 'banners'}
|
||||
<Banners
|
||||
saveHandler={() => {
|
||||
show = false;
|
||||
toast.success($i18n.t('Settings saved successfully!'));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
settings,
|
||||
showSidebar,
|
||||
tags as _tags,
|
||||
WEBUI_NAME
|
||||
WEBUI_NAME,
|
||||
banners
|
||||
} from '$lib/stores';
|
||||
import { convertMessagesToHistory, copyToClipboard, splitStream } from '$lib/utils';
|
||||
|
||||
@@ -45,7 +46,10 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { i18n as i18nType } from 'i18next';
|
||||
import { runWebSearch } from '$lib/apis/rag';
|
||||
|
||||
import Banner from '../common/Banner.svelte';
|
||||
import { getUserSettings } from '$lib/apis/users';
|
||||
|
||||
|
||||
const i18n: Writable<i18nType> = getContext('i18n');
|
||||
|
||||
export let chatIdProp = '';
|
||||
@@ -141,6 +145,7 @@
|
||||
} else if ($settings?.models) {
|
||||
selectedModels = $settings?.models;
|
||||
} else if ($config?.default_models) {
|
||||
console.log($config?.default_models.split(',') ?? '');
|
||||
selectedModels = $config?.default_models.split(',');
|
||||
} else {
|
||||
selectedModels = [''];
|
||||
@@ -159,10 +164,13 @@
|
||||
$models.map((m) => m.id).includes(modelId) ? modelId : ''
|
||||
);
|
||||
|
||||
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
settings.set({
|
||||
..._settings
|
||||
});
|
||||
const userSettings = await getUserSettings(localStorage.token);
|
||||
|
||||
if (userSettings) {
|
||||
settings.set(userSettings.ui);
|
||||
} else {
|
||||
settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
||||
}
|
||||
|
||||
const chatInput = document.getElementById('chat-textarea');
|
||||
setTimeout(() => chatInput?.focus(), 0);
|
||||
@@ -192,12 +200,20 @@
|
||||
: convertMessagesToHistory(chatContent.messages);
|
||||
title = chatContent.title;
|
||||
|
||||
let _settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
const userSettings = await getUserSettings(localStorage.token);
|
||||
|
||||
if (userSettings) {
|
||||
await settings.set(userSettings.ui);
|
||||
} else {
|
||||
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
|
||||
}
|
||||
|
||||
await settings.set({
|
||||
..._settings,
|
||||
system: chatContent.system ?? _settings.system,
|
||||
params: chatContent.options ?? _settings.params
|
||||
...$settings,
|
||||
system: chatContent.system ?? $settings.system,
|
||||
params: chatContent.options ?? $settings.params
|
||||
});
|
||||
|
||||
autoScroll = true;
|
||||
await tick();
|
||||
|
||||
@@ -1073,6 +1089,34 @@
|
||||
{chat}
|
||||
{initNewChat}
|
||||
/>
|
||||
|
||||
{#if $banners.length > 0 && !$chatId && selectedModels.length <= 1}
|
||||
<div
|
||||
class="absolute top-[4.25rem] w-full {$showSidebar ? 'md:max-w-[calc(100%-260px)]' : ''}"
|
||||
>
|
||||
<div class=" flex flex-col gap-1 w-full">
|
||||
{#each $banners.filter( (b) => (b.dismissible ? !JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]').includes(b.id) : true) ) as banner}
|
||||
<Banner
|
||||
{banner}
|
||||
on:dismiss={(e) => {
|
||||
const bannerId = e.detail;
|
||||
|
||||
localStorage.setItem(
|
||||
'dismissedBannerIds',
|
||||
JSON.stringify(
|
||||
[
|
||||
bannerId,
|
||||
...JSON.parse(localStorage.getItem('dismissedBannerIds') ?? '[]')
|
||||
].filter((id) => $banners.find((b) => b.id === id))
|
||||
)
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col flex-auto">
|
||||
<div
|
||||
class=" pb-2.5 flex flex-col justify-between w-full flex-auto overflow-auto h-0 max-w-full"
|
||||
|
||||
@@ -797,7 +797,7 @@
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
{#if $config.enable_image_generation && !readOnly}
|
||||
{#if $config?.features.enable_image_generation && !readOnly}
|
||||
<Tooltip content="Generate Image" placement="bottom">
|
||||
<button
|
||||
class="{isLastMessage
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Collapsible } from 'bits-ui';
|
||||
|
||||
import { setDefaultModels } from '$lib/apis/configs';
|
||||
import { models, showSettings, settings, user, mobile } from '$lib/stores';
|
||||
import { onMount, tick, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Selector from './ModelSelector/Selector.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
|
||||
import { setDefaultModels } from '$lib/apis/configs';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let selectedModels = [''];
|
||||
@@ -22,12 +22,8 @@
|
||||
return;
|
||||
}
|
||||
settings.set({ ...$settings, models: selectedModels });
|
||||
localStorage.setItem('settings', JSON.stringify($settings));
|
||||
await updateUserSettings(localStorage.token, { ui: $settings });
|
||||
|
||||
if ($user.role === 'admin') {
|
||||
console.log('setting default models globally');
|
||||
await setDefaultModels(localStorage.token, selectedModels.join(','));
|
||||
}
|
||||
toast.success($i18n.t('Default model updated'));
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getVersionUpdates } from '$lib/apis';
|
||||
import { getOllamaVersion } from '$lib/apis/ollama';
|
||||
import { WEBUI_VERSION } from '$lib/constants';
|
||||
import { WEBUI_BUILD_HASH, WEBUI_VERSION } from '$lib/constants';
|
||||
import { WEBUI_NAME, config, showChangelog } from '$lib/stores';
|
||||
import { compareVersion } from '$lib/utils';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
@@ -54,7 +54,7 @@
|
||||
<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 gap-1">
|
||||
<Tooltip content={WEBUI_VERSION === '0.1.117' ? "🪖 We're just getting started." : ''}>
|
||||
<Tooltip content={WEBUI_BUILD_HASH}>
|
||||
v{WEBUI_VERSION}
|
||||
</Tooltip>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getAudioConfig, updateAudioConfig } from '$lib/apis/audio';
|
||||
import { user } from '$lib/stores';
|
||||
import { user, settings } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -99,16 +99,14 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
conversationMode = $settings.conversationMode ?? false;
|
||||
speechAutoSend = $settings.speechAutoSend ?? false;
|
||||
responseAutoPlayback = $settings.responseAutoPlayback ?? false;
|
||||
|
||||
conversationMode = settings.conversationMode ?? false;
|
||||
speechAutoSend = settings.speechAutoSend ?? false;
|
||||
responseAutoPlayback = settings.responseAutoPlayback ?? false;
|
||||
|
||||
STTEngine = settings?.audio?.STTEngine ?? '';
|
||||
TTSEngine = settings?.audio?.TTSEngine ?? '';
|
||||
speaker = settings?.audio?.speaker ?? '';
|
||||
model = settings?.audio?.model ?? '';
|
||||
STTEngine = $settings?.audio?.STTEngine ?? '';
|
||||
TTSEngine = $settings?.audio?.TTSEngine ?? '';
|
||||
speaker = $settings?.audio?.speaker ?? '';
|
||||
model = $settings?.audio?.model ?? '';
|
||||
|
||||
if (TTSEngine === 'openai') {
|
||||
getOpenAIVoices();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import { chats, user, config } from '$lib/stores';
|
||||
import { chats, user, settings } from '$lib/stores';
|
||||
|
||||
import {
|
||||
archiveAllChats,
|
||||
@@ -99,9 +99,7 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
|
||||
saveChatHistory = settings.saveChatHistory ?? true;
|
||||
saveChatHistory = $settings.saveChatHistory ?? true;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
} from '$lib/apis/openai';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -31,8 +32,8 @@
|
||||
let OPENAI_API_KEYS = [''];
|
||||
let OPENAI_API_BASE_URLS = [''];
|
||||
|
||||
let ENABLE_OPENAI_API = false;
|
||||
let ENABLE_OLLAMA_API = false;
|
||||
let ENABLE_OPENAI_API = null;
|
||||
let ENABLE_OLLAMA_API = null;
|
||||
|
||||
const updateOpenAIHandler = async () => {
|
||||
OPENAI_API_BASE_URLS = await updateOpenAIUrls(localStorage.token, OPENAI_API_BASE_URLS);
|
||||
@@ -57,16 +58,23 @@
|
||||
|
||||
onMount(async () => {
|
||||
if ($user.role === 'admin') {
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
|
||||
const ollamaConfig = await getOllamaConfig(localStorage.token);
|
||||
const openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||
|
||||
ENABLE_OPENAI_API = openaiConfig.ENABLE_OPENAI_API;
|
||||
ENABLE_OLLAMA_API = ollamaConfig.ENABLE_OLLAMA_API;
|
||||
|
||||
OLLAMA_BASE_URLS = await getOllamaUrls(localStorage.token);
|
||||
|
||||
OPENAI_API_BASE_URLS = await getOpenAIUrls(localStorage.token);
|
||||
OPENAI_API_KEYS = await getOpenAIKeys(localStorage.token);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -78,202 +86,212 @@
|
||||
dispatch('save');
|
||||
}}
|
||||
>
|
||||
<div class=" pr-1.5 overflow-y-scroll max-h-[25rem] space-y-3">
|
||||
<div class=" space-y-3">
|
||||
<div class="mt-2 space-y-2 pr-1.5">
|
||||
<div class="space-y-3 pr-1.5 overflow-y-scroll h-[24rem] max-h-[25rem]">
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null}
|
||||
<div class=" space-y-3">
|
||||
<div class="mt-2 space-y-2 pr-1.5">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_OPENAI_API}
|
||||
on:change={async () => {
|
||||
updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OPENAI_API}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
bind:value={url}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={OPENAI_API_KEYS[idx]}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div class="self-center flex items-center">
|
||||
{#if idx === 0}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
|
||||
OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('WebUI will make requests to')}
|
||||
<span class=" text-gray-200">'{url}/models'</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class="pr-1.5 space-y-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_OPENAI_API}
|
||||
bind:state={ENABLE_OLLAMA_API}
|
||||
on:change={async () => {
|
||||
updateOpenAIConfig(localStorage.token, ENABLE_OPENAI_API);
|
||||
updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OPENAI_API}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="flex-1">
|
||||
{#if ENABLE_OLLAMA_API}
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<div class="flex gap-1.5">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
||||
bind:value={url}
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={OPENAI_API_KEYS[idx]}
|
||||
autocomplete="off"
|
||||
<div class="self-center flex items-center">
|
||||
{#if idx === 0}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button
|
||||
class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
on:click={() => {
|
||||
updateOllamaUrlsHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</div>
|
||||
<div class="self-center flex items-center">
|
||||
{#if idx === 0}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OPENAI_API_BASE_URLS = [...OPENAI_API_BASE_URLS, ''];
|
||||
OPENAI_API_KEYS = [...OPENAI_API_KEYS, ''];
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class=" mb-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('WebUI will make requests to')}
|
||||
<span class=" text-gray-200">'{url}/models'</span>
|
||||
</div>
|
||||
{/each}
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Trouble accessing Ollama?')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Click here for help.')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" dark:border-gray-700" />
|
||||
|
||||
<div class="pr-1.5 space-y-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_OLLAMA_API}
|
||||
on:change={async () => {
|
||||
updateOllamaConfig(localStorage.token, ENABLE_OLLAMA_API);
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-full justify-center">
|
||||
<div class="my-auto">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
</div>
|
||||
{#if ENABLE_OLLAMA_API}
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-2">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<div class="flex gap-1.5">
|
||||
<input
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
|
||||
placeholder={$i18n.t('Enter URL (e.g. http://localhost:11434)')}
|
||||
bind:value={url}
|
||||
/>
|
||||
|
||||
<div class="self-center flex items-center">
|
||||
{#if idx === 0}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OLLAMA_BASE_URLS = [...OLLAMA_BASE_URLS, ''];
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path d="M3.75 7.25a.75.75 0 0 0 0 1.5h8.5a.75.75 0 0 0 0-1.5h-8.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex">
|
||||
<button
|
||||
class="self-center p-2 bg-gray-200 hover:bg-gray-300 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
|
||||
on:click={() => {
|
||||
updateOllamaUrlsHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<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="M15.312 11.424a5.5 5.5 0 01-9.201 2.466l-.312-.311h2.433a.75.75 0 000-1.5H3.989a.75.75 0 00-.75.75v4.242a.75.75 0 001.5 0v-2.43l.31.31a7 7 0 0011.712-3.138.75.75 0 00-1.449-.39zm1.23-3.723a.75.75 0 00.219-.53V2.929a.75.75 0 00-1.5 0V5.36l-.31-.31A7 7 0 003.239 8.188a.75.75 0 101.448.389A5.5 5.5 0 0113.89 6.11l.311.31h-2.432a.75.75 0 000 1.5h4.243a.75.75 0 00.53-.219z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t('Trouble accessing Ollama?')}
|
||||
<a
|
||||
class=" text-gray-300 font-medium underline"
|
||||
href="https://github.com/open-webui/open-webui#troubleshooting"
|
||||
target="_blank"
|
||||
>
|
||||
{$i18n.t('Click here for help.')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-3 text-sm font-medium">
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { getLanguages } from '$lib/i18n';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import { models, user, theme } from '$lib/stores';
|
||||
import { models, settings, theme } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -71,23 +71,22 @@
|
||||
onMount(async () => {
|
||||
selectedTheme = localStorage.theme ?? 'system';
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
languages = await getLanguages();
|
||||
|
||||
notificationEnabled = settings.notificationEnabled ?? false;
|
||||
system = settings.system ?? '';
|
||||
notificationEnabled = $settings.notificationEnabled ?? false;
|
||||
system = $settings.system ?? '';
|
||||
|
||||
requestFormat = settings.requestFormat ?? '';
|
||||
keepAlive = settings.keepAlive ?? null;
|
||||
requestFormat = $settings.requestFormat ?? '';
|
||||
keepAlive = $settings.keepAlive ?? null;
|
||||
|
||||
params.seed = settings.seed ?? 0;
|
||||
params.temperature = settings.temperature ?? '';
|
||||
params.frequency_penalty = settings.frequency_penalty ?? '';
|
||||
params.top_k = settings.top_k ?? '';
|
||||
params.top_p = settings.top_p ?? '';
|
||||
params.num_ctx = settings.num_ctx ?? '';
|
||||
params = { ...params, ...settings.params };
|
||||
params.stop = settings?.params?.stop ? (settings?.params?.stop ?? []).join(',') : null;
|
||||
params.seed = $settings.seed ?? 0;
|
||||
params.temperature = $settings.temperature ?? '';
|
||||
params.frequency_penalty = $settings.frequency_penalty ?? '';
|
||||
params.top_k = $settings.top_k ?? '';
|
||||
params.top_p = $settings.top_p ?? '';
|
||||
params.num_ctx = $settings.num_ctx ?? '';
|
||||
params = { ...params, ...$settings.params };
|
||||
params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null;
|
||||
});
|
||||
|
||||
const applyTheme = (_theme: string) => {
|
||||
|
||||
@@ -105,23 +105,18 @@
|
||||
promptSuggestions = $config?.default_prompt_suggestions;
|
||||
}
|
||||
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
|
||||
titleAutoGenerate = settings?.title?.auto ?? true;
|
||||
titleAutoGenerateModel = settings?.title?.model ?? '';
|
||||
titleAutoGenerateModelExternal = settings?.title?.modelExternal ?? '';
|
||||
titleAutoGenerate = $settings?.title?.auto ?? true;
|
||||
titleAutoGenerateModel = $settings?.title?.model ?? '';
|
||||
titleAutoGenerateModelExternal = $settings?.title?.modelExternal ?? '';
|
||||
titleGenerationPrompt =
|
||||
settings?.title?.prompt ??
|
||||
$i18n.t(
|
||||
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':"
|
||||
) + ' {{prompt}}';
|
||||
|
||||
responseAutoCopy = settings.responseAutoCopy ?? false;
|
||||
showUsername = settings.showUsername ?? false;
|
||||
chatBubble = settings.chatBubble ?? true;
|
||||
fullScreenMode = settings.fullScreenMode ?? false;
|
||||
splitLargeChunks = settings.splitLargeChunks ?? false;
|
||||
chatDirection = settings.chatDirection ?? 'LTR';
|
||||
$settings?.title?.prompt ??
|
||||
`Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title': {{prompt}}`;
|
||||
responseAutoCopy = $settings.responseAutoCopy ?? false;
|
||||
showUsername = $settings.showUsername ?? false;
|
||||
chatBubble = $settings.chatBubble ?? true;
|
||||
fullScreenMode = $settings.fullScreenMode ?? false;
|
||||
splitLargeChunks = $settings.splitLargeChunks ?? false;
|
||||
chatDirection = $settings.chatDirection ?? 'LTR';
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
let enableMemory = false;
|
||||
|
||||
onMount(async () => {
|
||||
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
|
||||
enableMemory = settings?.memory ?? false;
|
||||
enableMemory = $settings?.memory ?? false;
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Images from './Settings/Images.svelte';
|
||||
import User from '../icons/User.svelte';
|
||||
import Personalization from './Settings/Personalization.svelte';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -26,7 +27,7 @@
|
||||
console.log(updated);
|
||||
await settings.set({ ...$settings, ...updated });
|
||||
await models.set(await getModels());
|
||||
localStorage.setItem('settings', JSON.stringify($settings));
|
||||
await updateUserSettings(localStorage.token, { ui: $settings });
|
||||
};
|
||||
|
||||
const getModels = async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from 'svelte';
|
||||
import { models } from '$lib/stores';
|
||||
import { models, config } from '$lib/stores';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { deleteSharedChatById, getChatById, shareChatById } from '$lib/apis/chats';
|
||||
@@ -134,16 +134,18 @@
|
||||
<div class="flex justify-end">
|
||||
<div class="flex flex-col items-end space-x-1 mt-1.5">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
shareChat();
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
</button>
|
||||
{#if $config?.features.enable_community_sharing}
|
||||
<button
|
||||
class=" self-center px-3.5 py-2 rounded-xl text-sm font-medium bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-white"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
shareChat();
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Share to OpenWebUI Community')}
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import type { Banner } from '$lib/types';
|
||||
import { onMount, createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let banner: Banner = {
|
||||
id: '',
|
||||
type: 'info',
|
||||
title: '',
|
||||
content: '',
|
||||
url: '',
|
||||
dismissable: true,
|
||||
timestamp: Math.floor(Date.now() / 1000)
|
||||
};
|
||||
|
||||
export let dismissed = false;
|
||||
|
||||
let mounted = false;
|
||||
|
||||
const classNames: Record<string, string> = {
|
||||
info: 'bg-blue-500/20 text-blue-700 dark:text-blue-200 ',
|
||||
success: 'bg-green-500/20 text-green-700 dark:text-green-200',
|
||||
warning: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-200',
|
||||
error: 'bg-red-500/20 text-red-700 dark:text-red-200'
|
||||
};
|
||||
|
||||
const dismiss = (id) => {
|
||||
dismissed = true;
|
||||
dispatch('dismiss', id);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !dismissed}
|
||||
{#if mounted}
|
||||
<div
|
||||
class=" top-0 left-0 right-0 p-2 mx-4 px-3 flex justify-center items-center relative rounded-xl border border-gray-100 dark:border-gray-850 text-gray-800 dark:text-gary-100 bg-white dark:bg-gray-900 backdrop-blur-xl z-40"
|
||||
transition:fade={{ delay: 100, duration: 300 }}
|
||||
>
|
||||
<div class=" flex flex-col md:flex-row md:items-center flex-1 text-sm w-fit gap-1.5">
|
||||
<div class="flex justify-between self-start">
|
||||
<div
|
||||
class=" text-xs font-black {classNames[banner.type] ??
|
||||
classNames['info']} w-fit px-2 rounded uppercase line-clamp-1 mr-0.5"
|
||||
>
|
||||
{banner.type}
|
||||
</div>
|
||||
|
||||
{#if banner.url}
|
||||
<div class="flex md:hidden group w-fit md:items-center">
|
||||
<a
|
||||
class="text-gray-700 dark:text-white text-xs font-bold underline"
|
||||
href="/assets/files/whitepaper.pdf"
|
||||
target="_blank">Learn More</a
|
||||
>
|
||||
|
||||
<div
|
||||
class=" ml-1 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white"
|
||||
>
|
||||
<!-- -->
|
||||
<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="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-white">
|
||||
{banner.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if banner.url}
|
||||
<div class="hidden md:flex group w-fit md:items-center">
|
||||
<a
|
||||
class="text-gray-700 dark:text-white text-xs font-bold underline"
|
||||
href="/"
|
||||
target="_blank">Learn More</a
|
||||
>
|
||||
|
||||
<div class=" ml-1 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-white">
|
||||
<!-- -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 16"
|
||||
fill="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex self-start">
|
||||
{#if banner.dismissible}
|
||||
<button
|
||||
on:click={() => {
|
||||
dismiss(banner.id);
|
||||
}}
|
||||
class=" -mt-[3px] ml-1.5 mr-1 text-gray-400 dark:hover:text-white h-1">×</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -33,6 +33,7 @@
|
||||
import ArchiveBox from '../icons/ArchiveBox.svelte';
|
||||
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
|
||||
import UserMenu from './Sidebar/UserMenu.svelte';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
|
||||
const BREAKPOINT = 768;
|
||||
|
||||
@@ -183,7 +184,7 @@
|
||||
|
||||
const saveSettings = async (updated) => {
|
||||
await settings.set({ ...$settings, ...updated });
|
||||
localStorage.setItem('settings', JSON.stringify($settings));
|
||||
await updateUserSettings(localStorage.token, { ui: $settings });
|
||||
location.href = '/';
|
||||
};
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
<hr class=" dark:border-gray-850 my-2" />
|
||||
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
|
||||
{#if chats.length > 0}
|
||||
<div>
|
||||
<div class="w-full">
|
||||
<div class="text-left text-sm w-full mb-3 max-h-[22rem] overflow-y-scroll">
|
||||
<div class="relative overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
|
||||
|
||||
Reference in New Issue
Block a user