Merge branch 'dev' into update-ga-strings-june
This commit is contained in:
@@ -347,6 +347,8 @@ export const userSignOut = async () => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
sessionStorage.clear();
|
||||
return res;
|
||||
};
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ export const importChat = async (
|
||||
chat: object,
|
||||
meta: object | null,
|
||||
pinned?: boolean,
|
||||
folderId?: string | null
|
||||
folderId?: string | null,
|
||||
createdAt: number | null = null,
|
||||
updatedAt: number | null = null
|
||||
) => {
|
||||
let error = null;
|
||||
|
||||
@@ -52,7 +54,9 @@ export const importChat = async (
|
||||
chat: chat,
|
||||
meta: meta ?? {},
|
||||
pinned: pinned,
|
||||
folder_id: folderId
|
||||
folder_id: folderId,
|
||||
created_at: createdAt ?? null,
|
||||
updated_at: updatedAt ?? null
|
||||
})
|
||||
})
|
||||
.then(async (res) => {
|
||||
|
||||
@@ -58,10 +58,10 @@ export const exportConfig = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const getDirectConnectionsConfig = async (token: string) => {
|
||||
export const getConnectionsConfig = async (token: string) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -85,10 +85,10 @@ export const getDirectConnectionsConfig = async (token: string) => {
|
||||
return res;
|
||||
};
|
||||
|
||||
export const setDirectConnectionsConfig = async (token: string, config: object) => {
|
||||
export const setConnectionsConfig = async (token: string, config: object) => {
|
||||
let error = null;
|
||||
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/direct_connections`, {
|
||||
const res = await fetch(`${WEBUI_API_BASE_URL}/configs/connections`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
+18
-8
@@ -8,17 +8,26 @@ import { toast } from 'svelte-sonner';
|
||||
export const getModels = async (
|
||||
token: string = '',
|
||||
connections: object | null = null,
|
||||
base: boolean = false
|
||||
base: boolean = false,
|
||||
refresh: boolean = false
|
||||
) => {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (refresh) {
|
||||
searchParams.append('refresh', 'true');
|
||||
}
|
||||
|
||||
let error = null;
|
||||
const res = await fetch(`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
const res = await fetch(
|
||||
`${WEBUI_BASE_URL}/api/models${base ? '/base' : ''}?${searchParams.toString()}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { authorization: `Bearer ${token}` })
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
.then(async (res) => {
|
||||
if (!res.ok) throw await res.json();
|
||||
return res.json();
|
||||
@@ -1587,6 +1596,7 @@ export interface ModelConfig {
|
||||
}
|
||||
|
||||
export interface ModelMeta {
|
||||
toolIds: never[];
|
||||
description?: string;
|
||||
capabilities?: object;
|
||||
profile_image_url?: string;
|
||||
|
||||
@@ -403,6 +403,7 @@ export const deleteUserById = async (token: string, userId: string) => {
|
||||
};
|
||||
|
||||
type UserUpdateForm = {
|
||||
role: string;
|
||||
profile_image_url: string;
|
||||
email: string;
|
||||
name: string;
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tags from './common/Tags.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -208,17 +210,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -524,29 +516,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
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';
|
||||
@@ -14,6 +15,8 @@
|
||||
import { getToolServerData } from '$lib/apis';
|
||||
import { verifyToolServerConnection } from '$lib/apis/configs';
|
||||
import AccessControl from './workspace/common/AccessControl.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -153,29 +156,21 @@
|
||||
<Modal size="sm" bind:show>
|
||||
<div>
|
||||
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-2">
|
||||
<div class=" text-lg font-medium self-center font-primary">
|
||||
<h1 class=" text-lg font-medium self-center font-primary">
|
||||
{#if edit}
|
||||
{$i18n.t('Edit Connection')}
|
||||
{:else}
|
||||
{$i18n.t('Add Connection')}
|
||||
{/if}
|
||||
</div>
|
||||
</h1>
|
||||
<button
|
||||
class="self-center"
|
||||
aria-label={$i18n.t('Close Configure Connection Modal')}
|
||||
on:click={() => {
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -192,12 +187,17 @@
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class="flex justify-between mb-0.5">
|
||||
<div class=" text-xs text-gray-500">{$i18n.t('URL')}</div>
|
||||
<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
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center">
|
||||
<input
|
||||
class="w-full flex-1 text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
id="api-base-url"
|
||||
class={`w-full flex-1 text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
bind:value={url}
|
||||
placeholder={$i18n.t('API Base URL')}
|
||||
@@ -214,6 +214,7 @@
|
||||
on:click={() => {
|
||||
verifyHandler();
|
||||
}}
|
||||
aria-label={$i18n.t('Verify Connection')}
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
@@ -221,6 +222,7 @@
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-4 h-4"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
@@ -237,9 +239,13 @@
|
||||
</div>
|
||||
|
||||
<div class="flex-1 flex items-center">
|
||||
<label for="url-or-path" class="sr-only"
|
||||
>{$i18n.t('openapi.json URL or Path')}</label
|
||||
>
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
id="url-or-path"
|
||||
bind:value={path}
|
||||
placeholder={$i18n.t('openapi.json URL or Path')}
|
||||
autocomplete="off"
|
||||
@@ -249,7 +255,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
<div
|
||||
class={`text-xs mt-1 ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t(`WebUI will make requests to "{{url}}"`, {
|
||||
url: path.includes('://') ? path : `${url}${path.startsWith('/') ? '' : '/'}${path}`
|
||||
})}
|
||||
@@ -257,12 +265,17 @@
|
||||
|
||||
<div class="flex gap-2 mt-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" text-xs text-gray-500">{$i18n.t('Auth')}</div>
|
||||
<label
|
||||
for="select-bearer-or-session"
|
||||
class={`text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('Auth')}</label
|
||||
>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex-shrink-0 self-start">
|
||||
<select
|
||||
class="w-full text-sm bg-transparent dark:bg-gray-900 placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden pr-5"
|
||||
id="select-bearer-or-session"
|
||||
class={`w-full text-sm bg-transparent pr-5 ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
bind:value={auth_type}
|
||||
>
|
||||
<option value="bearer">Bearer</option>
|
||||
@@ -273,13 +286,14 @@
|
||||
<div class="flex flex-1 items-center">
|
||||
{#if auth_type === 'bearer'}
|
||||
<SensitiveInput
|
||||
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
bind:value={key}
|
||||
placeholder={$i18n.t('API Key')}
|
||||
required={false}
|
||||
/>
|
||||
{:else if auth_type === 'session'}
|
||||
<div class="text-xs text-gray-500 self-center translate-y-[1px]">
|
||||
<div
|
||||
class={`text-xs self-center translate-y-[1px] ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>
|
||||
{$i18n.t('Forwards system user session credentials to authenticate')}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -293,11 +307,16 @@
|
||||
|
||||
<div class="flex gap-2">
|
||||
<div class="flex flex-col w-full">
|
||||
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Name')}</div>
|
||||
<label
|
||||
for="enter-name"
|
||||
class={`mb-0.5 text-xs" ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
|
||||
>{$i18n.t('Name')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
id="enter-name"
|
||||
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
bind:value={name}
|
||||
placeholder={$i18n.t('Enter name')}
|
||||
@@ -309,11 +328,16 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mt-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Description')}</div>
|
||||
<label
|
||||
for="description"
|
||||
class={`mb-1 text-xs ${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100 placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700 text-gray-500'}`}
|
||||
>{$i18n.t('Description')}</label
|
||||
>
|
||||
|
||||
<div class="flex-1">
|
||||
<input
|
||||
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
|
||||
id="description"
|
||||
class={`w-full text-sm bg-transparent ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : 'outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
type="text"
|
||||
bind:value={description}
|
||||
placeholder={$i18n.t('Enter description')}
|
||||
@@ -357,29 +381,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import Modal from './common/Modal.svelte';
|
||||
import { updateUserSettings } from '$lib/apis/users';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -36,18 +37,11 @@
|
||||
localStorage.version = $config.version;
|
||||
show = false;
|
||||
}}
|
||||
aria-label={$i18n.t('Close')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<XMark className={'size-5'}>
|
||||
<p class="sr-only">{$i18n.t('Close')}</p>
|
||||
<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>
|
||||
</XMark>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center mt-1">
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { extractFrontmatter } from '$lib/utils';
|
||||
|
||||
export let show = false;
|
||||
@@ -69,16 +71,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -120,29 +113,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -2,16 +2,42 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { getFeedbackById } from '$lib/apis/evaluations';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let selectedFeedback = null;
|
||||
|
||||
export let onClose: () => void = () => {};
|
||||
|
||||
let loaded = false;
|
||||
|
||||
let feedbackData = null;
|
||||
|
||||
const close = () => {
|
||||
show = false;
|
||||
onClose();
|
||||
};
|
||||
|
||||
const init = async () => {
|
||||
loaded = false;
|
||||
feedbackData = null;
|
||||
if (selectedFeedback) {
|
||||
feedbackData = await getFeedbackById(localStorage.token, selectedFeedback.id).catch((err) => {
|
||||
toast.error(err);
|
||||
return null;
|
||||
});
|
||||
|
||||
console.log('Feedback Data:', selectedFeedback, feedbackData);
|
||||
}
|
||||
loaded = true;
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal size="sm" bind:show>
|
||||
@@ -22,58 +48,89 @@
|
||||
{$i18n.t('Feedback Details')}
|
||||
</div>
|
||||
<button class="self-center" on:click={close} aria-label="Close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</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 w-full">
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
|
||||
{#if loaded}
|
||||
<div class="flex flex-col w-full">
|
||||
{#if feedbackData}
|
||||
{@const messageId = feedbackData?.meta?.message_id}
|
||||
{@const messages = feedbackData?.snapshot?.chat?.chat?.history.messages}
|
||||
|
||||
<div class="flex-1">
|
||||
<span>{selectedFeedback?.data?.details?.rating ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Reason')}</div>
|
||||
{#if messages[messages[messageId]?.parentId]}
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class="mb-1 text-xs text-gray-500">{$i18n.t('Prompt')}</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<span>{selectedFeedback?.data?.reason || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 text-xs whitespace-pre-line break-words">
|
||||
<span>{messages[messages[messageId]?.parentId]?.content || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mb-2">
|
||||
{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{#each selectedFeedback?.data?.tags as tag}
|
||||
<span class="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-xs">{tag}</span
|
||||
{#if messages[messageId]}
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class="mb-1 text-xs text-gray-500">{$i18n.t('Response')}</div>
|
||||
<div
|
||||
class="flex-1 text-xs whitespace-pre-line break-words max-h-32 overflow-y-auto"
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span>-</span>
|
||||
<span>{messages[messageId]?.content || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Rating')}</div>
|
||||
|
||||
<div class="flex-1 text-xs">
|
||||
<span>{selectedFeedback?.data?.details?.rating ?? '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Reason')}</div>
|
||||
|
||||
<div class="flex-1 text-xs">
|
||||
<span>{selectedFeedback?.data?.reason || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col w-full mb-2">
|
||||
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Comment')}</div>
|
||||
|
||||
<div class="flex-1 text-xs">
|
||||
<span>{selectedFeedback?.data?.comment || '-'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if selectedFeedback?.data?.tags && selectedFeedback?.data?.tags.length}
|
||||
<div class="mb-2 -mx-1">
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
{#each selectedFeedback?.data?.tags as tag}
|
||||
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-[9px]"
|
||||
>{tag}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
on:click={close}
|
||||
>
|
||||
{$i18n.t('Close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
on:click={close}
|
||||
>
|
||||
{$i18n.t('Close')}
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center w-full h-32">
|
||||
<Spinner className={'size-5'} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -305,7 +305,7 @@
|
||||
<tbody class="">
|
||||
{#each paginatedFeedbacks as feedback (feedback.id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
on:click={() => openFeedbackModal(feedback)}
|
||||
>
|
||||
<td class=" py-0.5 text-right font-semibold">
|
||||
@@ -369,7 +369,7 @@
|
||||
{dayjs(feedback.updated_at * 1000).fromNow()}
|
||||
</td>
|
||||
|
||||
<td class=" px-3 py-1 text-right font-semibold">
|
||||
<td class=" px-3 py-1 text-right font-semibold" on:click={(e) => e.stopPropagation()}>
|
||||
<FeedbackMenu
|
||||
on:delete={(e) => {
|
||||
deleteFeedbackHandler(feedback.id);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
@@ -77,7 +77,7 @@
|
||||
let showLeaderboardModal = false;
|
||||
let selectedModel = null;
|
||||
|
||||
const openFeedbackModal = (model) => {
|
||||
const openLeaderboardModelModal = (model) => {
|
||||
showLeaderboardModal = true;
|
||||
selectedModel = model;
|
||||
};
|
||||
@@ -350,7 +350,7 @@
|
||||
<Tooltip content={$i18n.t('Re-rank models by topic similarity')}>
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<MagnifyingGlass className="size-3" />
|
||||
<Search className="size-3" />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
@@ -371,7 +371,7 @@
|
||||
{#if loadingLeaderboard}
|
||||
<div class=" absolute top-0 bottom-0 left-0 right-0 flex">
|
||||
<div class="m-auto">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -504,8 +504,8 @@
|
||||
<tbody class="">
|
||||
{#each sortedModels as model, modelIdx (model.id)}
|
||||
<tr
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 transition"
|
||||
on:click={() => openFeedbackModal(model)}
|
||||
class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-850/50 transition"
|
||||
on:click={() => openLeaderboardModelModal(model)}
|
||||
>
|
||||
<td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
|
||||
<div class=" line-clamp-1">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
export let feedbacks = [];
|
||||
export let onClose: () => void = () => {};
|
||||
const i18n = getContext('i18n');
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const close = () => {
|
||||
show = false;
|
||||
@@ -37,25 +38,16 @@
|
||||
{model.name}
|
||||
</div>
|
||||
<button class="self-center" on:click={close} aria-label="Close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-5 pb-4 dark:text-gray-200">
|
||||
<div class="mb-2">
|
||||
{#if topTags.length}
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<div class="flex flex-wrap gap-1 mt-1 -mx-1">
|
||||
{#each topTags as tagInfo}
|
||||
<span class="px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-xs">
|
||||
{tagInfo.tag} <span class="text-gray-500">({tagInfo.count})</span>
|
||||
<span class="px-2 py-0.5 rounded-full bg-gray-100 dark:bg-gray-850 text-xs">
|
||||
{tagInfo.tag} <span class="text-gray-500 font-medium">{tagInfo.count}</span>
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -63,7 +55,7 @@
|
||||
<span>-</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-end pt-3">
|
||||
<div class="flex justify-end pt-2">
|
||||
<button
|
||||
class="px-3.5 py-1.5 text-sm font-medium bg-black hover:bg-gray-900 text-white dark:bg-white dark:text-black dark:hover:bg-gray-100 transition rounded-full"
|
||||
type="button"
|
||||
|
||||
@@ -135,7 +135,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -161,7 +163,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -413,7 +417,9 @@
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections &&
|
||||
($settings?.directConnections ?? null)
|
||||
($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}}
|
||||
@@ -559,7 +565,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
}}
|
||||
@@ -585,7 +593,9 @@
|
||||
models.set(
|
||||
await getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
} from '$lib/apis/audio';
|
||||
import { config, settings } from '$lib/stores';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
|
||||
import { TTS_RESPONSE_SPLIT } from '$lib/types';
|
||||
@@ -373,33 +374,7 @@
|
||||
>
|
||||
{#if STT_WHISPER_MODEL_LOADING}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { getOllamaConfig, updateOllamaConfig } from '$lib/apis/ollama';
|
||||
import { getOpenAIConfig, updateOpenAIConfig, getOpenAIModels } from '$lib/apis/openai';
|
||||
import { getModels as _getModels } from '$lib/apis';
|
||||
import { getDirectConnectionsConfig, setDirectConnectionsConfig } from '$lib/apis/configs';
|
||||
import { getConnectionsConfig, setConnectionsConfig } from '$lib/apis/configs';
|
||||
|
||||
import { config, models, settings, user } from '$lib/stores';
|
||||
|
||||
@@ -25,7 +25,9 @@
|
||||
const getModels = async () => {
|
||||
const models = await _getModels(
|
||||
localStorage.token,
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
|
||||
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null),
|
||||
false,
|
||||
true
|
||||
);
|
||||
return models;
|
||||
};
|
||||
@@ -41,7 +43,7 @@
|
||||
let ENABLE_OPENAI_API: null | boolean = null;
|
||||
let ENABLE_OLLAMA_API: null | boolean = null;
|
||||
|
||||
let directConnectionsConfig = null;
|
||||
let connectionsConfig = null;
|
||||
|
||||
let pipelineUrls = {};
|
||||
let showAddOpenAIConnectionModal = false;
|
||||
@@ -104,15 +106,13 @@
|
||||
}
|
||||
};
|
||||
|
||||
const updateDirectConnectionsHandler = async () => {
|
||||
const res = await setDirectConnectionsConfig(localStorage.token, directConnectionsConfig).catch(
|
||||
(error) => {
|
||||
toast.error(`${error}`);
|
||||
}
|
||||
);
|
||||
const updateConnectionsHandler = async () => {
|
||||
const res = await setConnectionsConfig(localStorage.token, connectionsConfig).catch((error) => {
|
||||
toast.error(`${error}`);
|
||||
});
|
||||
|
||||
if (res) {
|
||||
toast.success($i18n.t('Direct Connections settings updated'));
|
||||
toast.success($i18n.t('Connections settings updated'));
|
||||
await models.set(await getModels());
|
||||
}
|
||||
};
|
||||
@@ -148,7 +148,7 @@
|
||||
openaiConfig = await getOpenAIConfig(localStorage.token);
|
||||
})(),
|
||||
(async () => {
|
||||
directConnectionsConfig = await getDirectConnectionsConfig(localStorage.token);
|
||||
connectionsConfig = await getConnectionsConfig(localStorage.token);
|
||||
})()
|
||||
]);
|
||||
|
||||
@@ -215,36 +215,103 @@
|
||||
|
||||
<form class="flex flex-col h-full justify-between text-sm" on:submit|preventDefault={submitHandler}>
|
||||
<div class=" overflow-y-scroll scrollbar-hidden h-full">
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && directConnectionsConfig !== null}
|
||||
<div class="my-2">
|
||||
<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>
|
||||
{#if ENABLE_OPENAI_API !== null && ENABLE_OLLAMA_API !== null && connectionsConfig !== null}
|
||||
<div class="mb-3.5">
|
||||
<div class=" mb-2.5 text-base font-medium">{$i18n.t('General')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={ENABLE_OPENAI_API}
|
||||
on:change={async () => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="my-2">
|
||||
<div class="mt-2 space-y-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('OpenAI API')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={ENABLE_OPENAI_API}
|
||||
on:change={async () => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OPENAI_API}
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium text-xs">{$i18n.t('Manage OpenAI API Connections')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showAddOpenAIConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||
<OpenAIConnection
|
||||
pipeline={pipelineUrls[url] ? true : false}
|
||||
bind:url
|
||||
bind:key={OPENAI_API_KEYS[idx]}
|
||||
bind:config={OPENAI_API_CONFIGS[idx]}
|
||||
onSubmit={() => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
|
||||
let newConfig = {};
|
||||
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] =
|
||||
OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OPENAI_API_CONFIGS = newConfig;
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=" my-2">
|
||||
<div class="flex justify-between items-center text-sm mb-2">
|
||||
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_OLLAMA_API}
|
||||
on:change={async () => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OPENAI_API}
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
{#if ENABLE_OLLAMA_API}
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium">{$i18n.t('Manage OpenAI API Connections')}</div>
|
||||
<div class="font-medium text-xs">{$i18n.t('Manage Ollama API Connections')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showAddOpenAIConnectionModal = true;
|
||||
showAddOllamaConnectionModal = true;
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
@@ -253,133 +320,89 @@
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OPENAI_API_BASE_URLS as url, idx}
|
||||
<OpenAIConnection
|
||||
pipeline={pipelineUrls[url] ? true : false}
|
||||
bind:url
|
||||
bind:key={OPENAI_API_KEYS[idx]}
|
||||
bind:config={OPENAI_API_CONFIGS[idx]}
|
||||
onSubmit={() => {
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OPENAI_API_BASE_URLS = OPENAI_API_BASE_URLS.filter(
|
||||
(url, urlIdx) => idx !== urlIdx
|
||||
);
|
||||
OPENAI_API_KEYS = OPENAI_API_KEYS.filter((key, keyIdx) => idx !== keyIdx);
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<OllamaConnection
|
||||
bind:url
|
||||
bind:config={OLLAMA_API_CONFIGS[idx]}
|
||||
{idx}
|
||||
onSubmit={() => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||
|
||||
let newConfig = {};
|
||||
OPENAI_API_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] = OPENAI_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OPENAI_API_CONFIGS = newConfig;
|
||||
updateOpenAIHandler();
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
let newConfig = {};
|
||||
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] =
|
||||
OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OLLAMA_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 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>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
<div class="my-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
|
||||
|
||||
<div class="pr-1.5 my-2">
|
||||
<div class="flex justify-between items-center text-sm mb-2">
|
||||
<div class=" font-medium">{$i18n.t('Ollama API')}</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<Switch
|
||||
bind:state={ENABLE_OLLAMA_API}
|
||||
on:change={async () => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if ENABLE_OLLAMA_API}
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="font-medium">{$i18n.t('Manage Ollama API Connections')}</div>
|
||||
|
||||
<Tooltip content={$i18n.t(`Add Connection`)}>
|
||||
<button
|
||||
class="px-1"
|
||||
on:click={() => {
|
||||
showAddOllamaConnectionModal = true;
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={connectionsConfig.ENABLE_DIRECT_CONNECTIONS}
|
||||
on:change={async () => {
|
||||
updateConnectionsHandler();
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class="flex w-full gap-1.5">
|
||||
<div class="flex-1 flex flex-col gap-1.5 mt-1.5">
|
||||
{#each OLLAMA_BASE_URLS as url, idx}
|
||||
<OllamaConnection
|
||||
bind:url
|
||||
bind:config={OLLAMA_API_CONFIGS[idx]}
|
||||
{idx}
|
||||
onSubmit={() => {
|
||||
updateOllamaHandler();
|
||||
}}
|
||||
onDelete={() => {
|
||||
OLLAMA_BASE_URLS = OLLAMA_BASE_URLS.filter((url, urlIdx) => idx !== urlIdx);
|
||||
|
||||
let newConfig = {};
|
||||
OLLAMA_BASE_URLS.forEach((url, newIdx) => {
|
||||
newConfig[newIdx] = OLLAMA_API_CONFIGS[newIdx < idx ? newIdx : newIdx + 1];
|
||||
});
|
||||
OLLAMA_API_CONFIGS = newConfig;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 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>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr class=" border-gray-100 dark:border-gray-850" />
|
||||
|
||||
<div class="pr-1.5 my-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" font-medium">{$i18n.t('Direct Connections')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={directConnectionsConfig.ENABLE_DIRECT_CONNECTIONS}
|
||||
on:change={async () => {
|
||||
updateDirectConnectionsHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1.5">
|
||||
<div class="text-xs text-gray-500">
|
||||
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
|
||||
|
||||
<div class="my-2">
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<div class=" text-xs font-medium">{$i18n.t('Cache Base Model List')}</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<div class="">
|
||||
<Switch
|
||||
bind:state={connectionsConfig.ENABLE_BASE_MODELS_CACHE}
|
||||
on:change={async () => {
|
||||
updateConnectionsHandler();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 text-xs text-gray-400 dark:text-gray-500">
|
||||
{$i18n.t(
|
||||
'Direct Connections allow users to connect to their own OpenAI compatible API endpoints.'
|
||||
'Base Model List Cache speeds up access by fetching base models only at startup or on settings save—faster, but may not show recent base model changes.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import ManageOllama from '../Models/Manage/ManageOllama.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let urlIdx: number | null = null;
|
||||
@@ -26,16 +27,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -90,10 +90,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (embeddingEngine === 'openai' && (OpenAIKey === '' || OpenAIUrl === '')) {
|
||||
toast.error($i18n.t('OpenAI URL/Key required.'));
|
||||
return;
|
||||
}
|
||||
if (
|
||||
embeddingEngine === 'azure_openai' &&
|
||||
(AzureOpenAIKey === '' || AzureOpenAIUrl === '' || AzureOpenAIVersion === '')
|
||||
@@ -731,7 +727,11 @@
|
||||
required
|
||||
/>
|
||||
|
||||
<SensitiveInput placeholder={$i18n.t('API Key')} bind:value={OpenAIKey} />
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('API Key')}
|
||||
bind:value={OpenAIKey}
|
||||
required={false}
|
||||
/>
|
||||
</div>
|
||||
{:else if embeddingEngine === 'ollama'}
|
||||
<div class="my-0.5 flex gap-2 pr-2">
|
||||
@@ -808,33 +808,7 @@
|
||||
>
|
||||
{#if updateEmbeddingModelLoading}
|
||||
<div class="self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/>
|
||||
<path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/>
|
||||
</svg>
|
||||
<Spinner />
|
||||
</div>
|
||||
{:else}
|
||||
<svg
|
||||
@@ -1272,7 +1246,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
</form>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { models } from '$lib/stores';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
@@ -11,6 +12,7 @@
|
||||
import { toast } from 'svelte-sonner';
|
||||
import AccessControl from '$lib/components/workspace/common/AccessControl.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let edit = false;
|
||||
@@ -141,16 +143,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -406,29 +399,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -90,7 +90,9 @@
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
checkForVersionUpdates();
|
||||
if (!$config?.offline_mode) {
|
||||
checkForVersionUpdates();
|
||||
}
|
||||
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
@@ -160,15 +162,17 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{#if !$config?.offline_mode}
|
||||
<button
|
||||
class=" text-xs px-3 py-1.5 bg-gray-50 hover:bg-gray-100 dark:bg-gray-850 dark:hover:bg-gray-800 transition rounded-lg font-medium"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
updateConfig,
|
||||
verifyConfigUrl
|
||||
} from '$lib/apis/images';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -504,7 +506,7 @@
|
||||
<div class=" mb-2 text-sm font-medium">{$i18n.t('ComfyUI Workflow')}</div>
|
||||
|
||||
{#if config.comfyui.COMFYUI_WORKFLOW}
|
||||
<textarea
|
||||
<Textarea
|
||||
class="w-full rounded-lg mb-1 py-2 px-4 text-xs bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden disabled:text-gray-600 resize-none"
|
||||
rows="10"
|
||||
bind:value={config.comfyui.COMFYUI_WORKFLOW}
|
||||
@@ -533,7 +535,7 @@
|
||||
/>
|
||||
|
||||
<button
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-100 border border-dashed dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
class="w-full text-sm font-medium py-2 bg-transparent hover:bg-gray-50 border border-dashed border-gray-50 dark:border-gray-850 dark:hover:bg-gray-850 text-center rounded-xl"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
document.getElementById('upload-comfyui-workflow-input')?.click();
|
||||
@@ -555,10 +557,10 @@
|
||||
|
||||
<div class="text-xs flex flex-col gap-1.5">
|
||||
{#each requiredWorkflowNodes as node}
|
||||
<div class="flex w-full items-center border dark:border-gray-850 rounded-lg">
|
||||
<div class="flex w-full items-center">
|
||||
<div class="shrink-0">
|
||||
<div
|
||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center rounded-l-lg bg-green-500/10 text-green-700 dark:text-green-200"
|
||||
class=" capitalize line-clamp-1 font-medium px-3 py-1 w-20 text-center bg-green-500/10 text-green-700 dark:text-green-200"
|
||||
>
|
||||
{node.type}{node.type === 'prompt' ? '*' : ''}
|
||||
</div>
|
||||
@@ -566,7 +568,7 @@
|
||||
<div class="">
|
||||
<Tooltip content="Input Key (e.g. text, unet_name, steps)">
|
||||
<input
|
||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r dark:border-gray-850"
|
||||
class="py-1 px-3 w-24 text-xs text-center bg-transparent outline-hidden border-r border-gray-50 dark:border-gray-850"
|
||||
placeholder="Key"
|
||||
bind:value={node.key}
|
||||
required
|
||||
@@ -580,7 +582,7 @@
|
||||
placement="top-start"
|
||||
>
|
||||
<input
|
||||
class="w-full py-1 px-4 rounded-r-lg text-xs bg-transparent outline-hidden"
|
||||
class="w-full py-1 px-4 text-xs bg-transparent outline-hidden"
|
||||
placeholder="Node Ids"
|
||||
bind:value={node.node_ids}
|
||||
/>
|
||||
@@ -711,29 +713,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -390,7 +390,7 @@
|
||||
|
||||
<div class="mb-2.5">
|
||||
<div class="flex w-full justify-between">
|
||||
<div class=" self-center text-sm">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Banners')}
|
||||
</div>
|
||||
|
||||
@@ -432,7 +432,7 @@
|
||||
{#if $user?.role === 'admin'}
|
||||
<div class=" space-y-3">
|
||||
<div class="flex w-full justify-between mb-2">
|
||||
<div class=" self-center text-sm">
|
||||
<div class=" self-center text-xs">
|
||||
{$i18n.t('Default Prompt Suggestions')}
|
||||
</div>
|
||||
|
||||
@@ -636,6 +636,6 @@
|
||||
</form>
|
||||
{:else}
|
||||
<div class=" h-full w-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import EllipsisVertical from '$lib/components/icons/EllipsisVertical.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Sortable from 'sortablejs';
|
||||
import { getContext } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
@@ -23,6 +25,13 @@
|
||||
});
|
||||
};
|
||||
|
||||
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'
|
||||
};
|
||||
|
||||
$: if (banners) {
|
||||
init();
|
||||
}
|
||||
@@ -44,14 +53,14 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class=" flex flex-col space-y-0.5" bind:this={bannerListElement}>
|
||||
<div class=" flex flex-col gap-3 {banners?.length > 0 ? 'mt-2' : ''}" bind:this={bannerListElement}>
|
||||
{#each banners as banner, bannerIdx (banner.id)}
|
||||
<div class=" flex justify-between items-center -ml-1" id="banner-item-{banner.id}">
|
||||
<div class=" flex justify-between items-start -ml-1" id="banner-item-{banner.id}">
|
||||
<EllipsisVertical className="size-4 cursor-move item-handle" />
|
||||
|
||||
<div class="flex flex-row flex-1 gap-2 items-center">
|
||||
<div class="flex flex-row flex-1 gap-2 items-start">
|
||||
<select
|
||||
class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden text-left pl-1 pr-2"
|
||||
class="w-fit capitalize rounded-xl text-xs bg-transparent outline-hidden pl-1 pr-5"
|
||||
bind:value={banner.type}
|
||||
required
|
||||
>
|
||||
@@ -64,10 +73,11 @@
|
||||
<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-hidden"
|
||||
<Textarea
|
||||
className="mr-2 text-xs w-full bg-transparent outline-hidden resize-none"
|
||||
placeholder={$i18n.t('Content')}
|
||||
bind:value={banner.content}
|
||||
maxSize={100}
|
||||
/>
|
||||
|
||||
<div class="relative -left-2">
|
||||
@@ -85,16 +95,7 @@
|
||||
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>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -563,6 +563,6 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<div class=" h-full w-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let initHandler = () => {};
|
||||
@@ -129,16 +130,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -278,29 +270,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
@@ -308,7 +278,7 @@
|
||||
</form>
|
||||
{:else}
|
||||
<div>
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1057,6 +1057,6 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-center items-center w-full h-full py-3">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import ManageOllama from './Manage/ManageOllama.svelte';
|
||||
import { getOllamaConfig } from '$lib/apis/ollama';
|
||||
@@ -48,16 +49,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
placeholder={$i18n.t('Enter Searxng Query URL')}
|
||||
bind:value={webConfig.SEARXNG_QUERY_URL}
|
||||
autocomplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -248,7 +249,6 @@
|
||||
bind:value={webConfig.KAGI_SEARCH_API_KEY}
|
||||
/>
|
||||
</div>
|
||||
.
|
||||
</div>
|
||||
{:else if webConfig.WEB_SEARCH_ENGINE === 'mojeek'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
import UsersSolid from '$lib/components/icons/UsersSolid.svelte';
|
||||
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import User from '$lib/components/icons/User.svelte';
|
||||
import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
|
||||
import GroupModal from './Groups/EditGroupModal.svelte';
|
||||
@@ -159,18 +160,7 @@
|
||||
<div class=" flex w-full space-x-2">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center ml-1 mr-3">
|
||||
<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Search />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Textarea from '$lib/components/common/Textarea.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
export let onSubmit: Function = () => {};
|
||||
export let show = false;
|
||||
|
||||
@@ -45,16 +47,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -111,29 +104,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Display from './Display.svelte';
|
||||
import Permissions from './Permissions.svelte';
|
||||
@@ -10,6 +11,7 @@
|
||||
import UserPlusSolid from '$lib/components/icons/UserPlusSolid.svelte';
|
||||
import WrenchSolid from '$lib/components/icons/WrenchSolid.svelte';
|
||||
import ConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let onSubmit: Function = () => {};
|
||||
export let onDelete: Function = () => {};
|
||||
@@ -124,16 +126,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -305,29 +298,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
import Checkbox from '$lib/components/common/Checkbox.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
|
||||
export let users = [];
|
||||
export let userIds = [];
|
||||
@@ -50,18 +51,7 @@
|
||||
<div class="flex w-full">
|
||||
<div class="flex flex-1">
|
||||
<div class=" self-center mr-3">
|
||||
<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="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<Search />
|
||||
</div>
|
||||
<input
|
||||
class=" w-full text-sm pr-4 rounded-r-xl outline-hidden bg-transparent"
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
|
||||
{#if users === null || total === null}
|
||||
<div class="my-10">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { generateInitialsImage } from '$lib/utils';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -132,16 +134,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -293,29 +286,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -54,16 +55,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import heic2any from 'heic2any';
|
||||
|
||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||
|
||||
@@ -78,7 +79,7 @@
|
||||
};
|
||||
|
||||
const inputFilesHandler = async (inputFiles) => {
|
||||
inputFiles.forEach((file) => {
|
||||
inputFiles.forEach(async (file) => {
|
||||
console.info('Processing file:', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
@@ -102,43 +103,50 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
||||
) {
|
||||
if (file['type'].startsWith('image/')) {
|
||||
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||
// Quick shortcut so we don’t do unnecessary work.
|
||||
const settingsCompression = settings?.imageCompression ?? false;
|
||||
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||
|
||||
// If neither settings nor config wants compression, return original URL.
|
||||
if (!settingsCompression && !configWidth && !configHeight) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Default to null (no compression unless set)
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
// If user/settings want compression, pick their preferred size.
|
||||
if (settingsCompression) {
|
||||
width = settings?.imageCompressionSize?.width ?? null;
|
||||
height = settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
// Apply config limits as an upper bound if any
|
||||
if (configWidth && (width === null || width > configWidth)) {
|
||||
width = configWidth;
|
||||
}
|
||||
if (configHeight && (height === null || height > configHeight)) {
|
||||
height = configHeight;
|
||||
}
|
||||
|
||||
// Do the compression if required
|
||||
if (width || height) {
|
||||
return await compressImage(imageUrl, width, height);
|
||||
}
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = async (event) => {
|
||||
let imageUrl = event.target.result;
|
||||
|
||||
if (
|
||||
($settings?.imageCompression ?? false) ||
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
if ($settings?.imageCompression ?? false) {
|
||||
width = $settings?.imageCompressionSize?.width ?? null;
|
||||
height = $settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
if (width > ($config?.file?.image_compression?.width ?? null)) {
|
||||
width = $config?.file?.image_compression?.width ?? null;
|
||||
}
|
||||
if (height > ($config?.file?.image_compression?.height ?? null)) {
|
||||
height = $config?.file?.image_compression?.height ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (width || height) {
|
||||
imageUrl = await compressImage(imageUrl, width, height);
|
||||
}
|
||||
}
|
||||
// Compress the image if settings or config require it
|
||||
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||
|
||||
files = [
|
||||
...files,
|
||||
@@ -149,7 +157,11 @@
|
||||
];
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
reader.readAsDataURL(
|
||||
file['type'] === 'image/heic'
|
||||
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
||||
: file
|
||||
);
|
||||
} else {
|
||||
uploadFileHandler(file);
|
||||
}
|
||||
|
||||
@@ -55,10 +55,7 @@
|
||||
|
||||
import { generateChatCompletion } from '$lib/apis/ollama';
|
||||
import {
|
||||
addTagById,
|
||||
createNewChat,
|
||||
deleteTagById,
|
||||
deleteTagsById,
|
||||
getAllTags,
|
||||
getChatById,
|
||||
getChatList,
|
||||
@@ -153,10 +150,10 @@
|
||||
webSearchEnabled = false;
|
||||
imageGenerationEnabled = false;
|
||||
|
||||
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
try {
|
||||
const input = JSON.parse(
|
||||
localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
||||
sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
||||
);
|
||||
|
||||
if (!$temporaryChatEnabled) {
|
||||
@@ -446,7 +443,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
if (sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
|
||||
prompt = '';
|
||||
files = [];
|
||||
selectedToolIds = [];
|
||||
@@ -457,7 +454,7 @@
|
||||
|
||||
try {
|
||||
const input = JSON.parse(
|
||||
localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
||||
sessionStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)
|
||||
);
|
||||
|
||||
if (!$temporaryChatEnabled) {
|
||||
@@ -708,6 +705,10 @@
|
||||
//////////////////////////
|
||||
|
||||
const initNewChat = async () => {
|
||||
if ($user?.role !== 'admin' && $user?.permissions?.chat?.temporary_enforced) {
|
||||
await temporaryChatEnabled.set(true);
|
||||
}
|
||||
|
||||
const availableModels = $models
|
||||
.filter((m) => !(m?.info?.meta?.hidden ?? false))
|
||||
.map((m) => m.id);
|
||||
@@ -2120,12 +2121,12 @@
|
||||
onChange={(input) => {
|
||||
if (!$temporaryChatEnabled) {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(
|
||||
sessionStorage.setItem(
|
||||
`chat-input${$chatId ? `-${$chatId}` : ''}`,
|
||||
JSON.stringify(input)
|
||||
);
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
|
||||
sessionStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
@@ -2227,7 +2228,7 @@
|
||||
{:else if loading}
|
||||
<div class=" flex items-center justify-center h-full w-full">
|
||||
<div class="m-auto">
|
||||
<Spinner />
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
>
|
||||
<Tooltip
|
||||
content={marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description ?? ''
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
placement="right"
|
||||
>
|
||||
@@ -68,7 +70,7 @@
|
||||
|
||||
{#if $temporaryChatEnabled}
|
||||
<Tooltip
|
||||
content={$i18n.t('This chat won’t appear in history and your messages will not be saved.')}
|
||||
content={$i18n.t("This chat won't appear in history and your messages will not be saved.")}
|
||||
className="w-full flex justify-start mb-0.5"
|
||||
placement="top"
|
||||
>
|
||||
@@ -96,7 +98,9 @@
|
||||
class="mt-0.5 text-base font-normal text-gray-500 dark:text-gray-400 line-clamp-3 markdown"
|
||||
>
|
||||
{@html marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
</div>
|
||||
{#if models[selectedModelIdx]?.info?.meta?.user}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import FileItem from '$lib/components/common/FileItem.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
|
||||
import { user } from '$lib/stores';
|
||||
import { user, settings } from '$lib/stores';
|
||||
export let models = [];
|
||||
export let chatFiles = [];
|
||||
export let params = {};
|
||||
@@ -74,7 +74,9 @@
|
||||
<div class="" slot="content">
|
||||
<textarea
|
||||
bind:value={params.system}
|
||||
class="w-full text-xs py-1.5 bg-transparent outline-hidden resize-none"
|
||||
class="w-full text-xs outline-hidden resize-vertical {$settings.highContrastMode
|
||||
? 'border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-800 p-2.5'
|
||||
: 'py-1.5 bg-transparent'}"
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt')}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import heic2any from 'heic2any';
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
@@ -320,7 +321,7 @@
|
||||
return;
|
||||
}
|
||||
|
||||
inputFiles.forEach((file) => {
|
||||
inputFiles.forEach(async (file) => {
|
||||
console.log('Processing file:', {
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
@@ -344,46 +345,53 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
['image/gif', 'image/webp', 'image/jpeg', 'image/png', 'image/avif'].includes(file['type'])
|
||||
) {
|
||||
if (file['type'].startsWith('image/')) {
|
||||
if (visionCapableModels.length === 0) {
|
||||
toast.error($i18n.t('Selected model(s) do not support image inputs'));
|
||||
return;
|
||||
}
|
||||
|
||||
const compressImageHandler = async (imageUrl, settings = {}, config = {}) => {
|
||||
// Quick shortcut so we don’t do unnecessary work.
|
||||
const settingsCompression = settings?.imageCompression ?? false;
|
||||
const configWidth = config?.file?.image_compression?.width ?? null;
|
||||
const configHeight = config?.file?.image_compression?.height ?? null;
|
||||
|
||||
// If neither settings nor config wants compression, return original URL.
|
||||
if (!settingsCompression && !configWidth && !configHeight) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
// Default to null (no compression unless set)
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
// If user/settings want compression, pick their preferred size.
|
||||
if (settingsCompression) {
|
||||
width = settings?.imageCompressionSize?.width ?? null;
|
||||
height = settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
// Apply config limits as an upper bound if any
|
||||
if (configWidth && (width === null || width > configWidth)) {
|
||||
width = configWidth;
|
||||
}
|
||||
if (configHeight && (height === null || height > configHeight)) {
|
||||
height = configHeight;
|
||||
}
|
||||
|
||||
// Do the compression if required
|
||||
if (width || height) {
|
||||
return await compressImage(imageUrl, width, height);
|
||||
}
|
||||
return imageUrl;
|
||||
};
|
||||
|
||||
let reader = new FileReader();
|
||||
reader.onload = async (event) => {
|
||||
let imageUrl = event.target.result;
|
||||
|
||||
if (
|
||||
($settings?.imageCompression ?? false) ||
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
let width = null;
|
||||
let height = null;
|
||||
|
||||
if ($settings?.imageCompression ?? false) {
|
||||
width = $settings?.imageCompressionSize?.width ?? null;
|
||||
height = $settings?.imageCompressionSize?.height ?? null;
|
||||
}
|
||||
|
||||
if (
|
||||
($config?.file?.image_compression?.width ?? null) ||
|
||||
($config?.file?.image_compression?.height ?? null)
|
||||
) {
|
||||
if (width > ($config?.file?.image_compression?.width ?? null)) {
|
||||
width = $config?.file?.image_compression?.width ?? null;
|
||||
}
|
||||
if (height > ($config?.file?.image_compression?.height ?? null)) {
|
||||
height = $config?.file?.image_compression?.height ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
if (width || height) {
|
||||
imageUrl = await compressImage(imageUrl, width, height);
|
||||
}
|
||||
}
|
||||
imageUrl = await compressImageHandler(imageUrl, $settings, $config);
|
||||
|
||||
files = [
|
||||
...files,
|
||||
@@ -393,7 +401,11 @@
|
||||
}
|
||||
];
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
reader.readAsDataURL(
|
||||
file['type'] === 'image/heic'
|
||||
? await heic2any({ blob: file, toType: 'image/jpeg' })
|
||||
: file
|
||||
);
|
||||
} else {
|
||||
uploadFileHandler(file);
|
||||
}
|
||||
@@ -659,7 +671,7 @@
|
||||
<div class="relative flex items-center">
|
||||
<Image
|
||||
src={file.url}
|
||||
alt="input"
|
||||
alt=""
|
||||
imageClassName=" size-14 rounded-xl object-cover"
|
||||
/>
|
||||
{#if atSelectedModel ? visionCapableModels.length === 0 : selectedModels.length !== visionCapableModels.length}
|
||||
@@ -677,6 +689,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="size-4 fill-yellow-300"
|
||||
>
|
||||
<path
|
||||
@@ -690,8 +703,12 @@
|
||||
</div>
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
||||
class=" bg-white text-black border border-white rounded-full {($settings?.highContrastMode ??
|
||||
false)
|
||||
? ''
|
||||
: 'outline-hidden focus:outline-hidden group-hover:visible invisible transition'}"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Remove file')}
|
||||
on:click={() => {
|
||||
files.splice(fileIdx, 1);
|
||||
files = files;
|
||||
@@ -701,6 +718,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class="size-4"
|
||||
>
|
||||
<path
|
||||
@@ -1253,11 +1271,12 @@
|
||||
<button
|
||||
class="bg-transparent hover:bg-gray-100 text-gray-800 dark:text-white dark:hover:bg-gray-800 transition rounded-full p-1.5 outline-hidden focus:outline-hidden"
|
||||
type="button"
|
||||
aria-label="More"
|
||||
aria-label={$i18n.t('More Available Tools')}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
class="size-5"
|
||||
>
|
||||
@@ -1379,6 +1398,10 @@
|
||||
{#if showCodeInterpreterButton}
|
||||
<Tooltip content={$i18n.t('Execute code for analysis')} placement="top">
|
||||
<button
|
||||
aria-label={codeInterpreterEnabled
|
||||
? $i18n.t('Disable Code Interpreter')
|
||||
: $i18n.t('Enable Code Interpreter')}
|
||||
aria-pressed={codeInterpreterEnabled}
|
||||
on:click|preventDefault={() =>
|
||||
(codeInterpreterEnabled = !codeInterpreterEnabled)}
|
||||
type="button"
|
||||
@@ -1530,7 +1553,7 @@
|
||||
);
|
||||
}
|
||||
}}
|
||||
aria-label="Call"
|
||||
aria-label={$i18n.t('Voice mode')}
|
||||
>
|
||||
<Headphone className="size-5" />
|
||||
</button>
|
||||
|
||||
@@ -854,6 +854,7 @@
|
||||
</button>
|
||||
{:else}
|
||||
<div class="relative flex video-container w-full max-h-full pt-2 pb-4 md:py-6 px-2 h-full">
|
||||
<!-- svelte-ignore a11y-media-has-caption -->
|
||||
<video
|
||||
id="camera-feed"
|
||||
autoplay
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import { createEventDispatcher, tick, getContext, onMount } from 'svelte';
|
||||
import { createEventDispatcher, tick, getContext, onMount, onDestroy } from 'svelte';
|
||||
import { removeLastWordFromString, isValidHttpUrl } from '$lib/utils';
|
||||
import { knowledge } from '$lib/stores';
|
||||
|
||||
@@ -42,6 +42,24 @@
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||
};
|
||||
|
||||
let container;
|
||||
let adjustHeightDebounce;
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (container) {
|
||||
if (adjustHeightDebounce) {
|
||||
clearTimeout(adjustHeightDebounce);
|
||||
}
|
||||
|
||||
adjustHeightDebounce = setTimeout(() => {
|
||||
if (!container) return;
|
||||
|
||||
// Ensure the container is visible before adjusting height
|
||||
const rect = container.getBoundingClientRect();
|
||||
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
const confirmSelect = async (item) => {
|
||||
dispatch('select', item);
|
||||
|
||||
@@ -75,7 +93,18 @@
|
||||
await tick();
|
||||
};
|
||||
|
||||
const decodeString = (str: string) => {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
adjustHeight();
|
||||
|
||||
let legacy_documents = $knowledge
|
||||
.filter((item) => item?.meta?.document)
|
||||
.map((item) => ({
|
||||
@@ -155,13 +184,9 @@
|
||||
});
|
||||
});
|
||||
|
||||
const decodeString = (str: string) => {
|
||||
try {
|
||||
return decodeURIComponent(str);
|
||||
} catch (e) {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('resize', adjustHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0 || prompt.split(' ')?.at(0)?.substring(1).startsWith('http')}
|
||||
@@ -174,6 +199,7 @@
|
||||
<div
|
||||
class="m-1 overflow-y-auto p-1 rounded-r-xl space-y-0.5 scrollbar-hidden max-h-60"
|
||||
id="command-options-container"
|
||||
bind:this={container}
|
||||
>
|
||||
{#each filteredItems as item, idx}
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
|
||||
import { tick, getContext } from 'svelte';
|
||||
|
||||
import { models } from '$lib/stores';
|
||||
@@ -51,18 +51,44 @@
|
||||
selectedIdx = Math.min(selectedIdx + 1, filteredItems.length - 1);
|
||||
};
|
||||
|
||||
let container;
|
||||
let adjustHeightDebounce;
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (container) {
|
||||
if (adjustHeightDebounce) {
|
||||
clearTimeout(adjustHeightDebounce);
|
||||
}
|
||||
|
||||
adjustHeightDebounce = setTimeout(() => {
|
||||
if (!container) return;
|
||||
|
||||
// Ensure the container is visible before adjusting height
|
||||
const rect = container.getBoundingClientRect();
|
||||
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmSelect = async (model) => {
|
||||
command = '';
|
||||
dispatch('select', model);
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
adjustHeight();
|
||||
|
||||
await tick();
|
||||
const chatInputElement = document.getElementById('chat-input');
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
await tick();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('resize', adjustHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredItems.length > 0}
|
||||
@@ -75,6 +101,7 @@
|
||||
<div
|
||||
class="m-1 overflow-y-auto p-1 rounded-r-lg space-y-0.5 scrollbar-hidden max-h-60"
|
||||
id="command-options-container"
|
||||
bind:this={container}
|
||||
>
|
||||
{#each filteredItems as model, modelIdx}
|
||||
<button
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
getUserTimezone,
|
||||
getWeekday
|
||||
} from '$lib/utils';
|
||||
import { tick, getContext } from 'svelte';
|
||||
import { tick, getContext, onMount, onDestroy } from 'svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
@@ -38,6 +38,25 @@
|
||||
selectedPromptIdx = Math.min(selectedPromptIdx + 1, filteredPrompts.length - 1);
|
||||
};
|
||||
|
||||
let container;
|
||||
let adjustHeightDebounce;
|
||||
|
||||
const adjustHeight = () => {
|
||||
if (container) {
|
||||
if (adjustHeightDebounce) {
|
||||
clearTimeout(adjustHeightDebounce);
|
||||
}
|
||||
|
||||
adjustHeightDebounce = setTimeout(() => {
|
||||
if (!container) return;
|
||||
|
||||
// Ensure the container is visible before adjusting height
|
||||
const rect = container.getBoundingClientRect();
|
||||
container.style.maxHeight = Math.max(Math.min(240, rect.bottom - 100), 100) + 'px';
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPrompt = async (command) => {
|
||||
let text = command.content;
|
||||
|
||||
@@ -172,6 +191,15 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
window.addEventListener('resize', adjustHeight);
|
||||
adjustHeight();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('resize', adjustHeight);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if filteredPrompts.length > 0}
|
||||
@@ -184,6 +212,7 @@
|
||||
<div
|
||||
class="m-1 overflow-y-auto p-1 space-y-0.5 scrollbar-hidden max-h-60"
|
||||
id="command-options-container"
|
||||
bind:this={container}
|
||||
>
|
||||
{#each filteredPrompts as prompt, promptIdx}
|
||||
<button
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { blobToFile, calculateSHA256, extractCurlyBraceWords } from '$lib/utils';
|
||||
|
||||
import { transcribeAudio } from '$lib/apis/audio';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import LocalizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
@@ -406,16 +407,7 @@
|
||||
onCancel();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="3"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -438,6 +438,7 @@
|
||||
<Message
|
||||
{chatId}
|
||||
bind:history
|
||||
{selectedModels}
|
||||
messageId={message.id}
|
||||
idx={messageIdx}
|
||||
{user}
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
console.log('sources', sources);
|
||||
citations = sources.reduce((acc, source) => {
|
||||
if (Object.keys(source).length === 0) {
|
||||
return acc;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -67,16 +69,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Badge from '$lib/components/common/Badge.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let show = false;
|
||||
@@ -49,16 +50,7 @@
|
||||
codeExecution = null;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
export let id;
|
||||
export let content;
|
||||
export let history;
|
||||
export let selectedModels = [];
|
||||
|
||||
export let model = null;
|
||||
export let sources = null;
|
||||
|
||||
@@ -25,11 +27,10 @@
|
||||
export let preview = false;
|
||||
export let floatingButtons = true;
|
||||
|
||||
export let onSave = () => {};
|
||||
export let onSourceClick = () => {};
|
||||
export let onTaskClick = () => {};
|
||||
|
||||
export let onAddMessages = () => {};
|
||||
export let onSave = (e) => {};
|
||||
export let onSourceClick = (e) => {};
|
||||
export let onTaskClick = (e) => {};
|
||||
export let onAddMessages = (e) => {};
|
||||
|
||||
let contentContainerElement;
|
||||
|
||||
@@ -192,7 +193,11 @@
|
||||
<FloatingButtons
|
||||
bind:this={floatingButtonsElement}
|
||||
{id}
|
||||
model={model?.id}
|
||||
model={(selectedModels ?? []).includes(model?.id)
|
||||
? model?.id
|
||||
: (selectedModels ?? []).length > 0
|
||||
? selectedModels.at(0)
|
||||
: model?.id}
|
||||
messages={createMessagesList(history, id)}
|
||||
onAdd={({ modelId, parentId, messages }) => {
|
||||
console.log(modelId, parentId, messages);
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
let tokens = [];
|
||||
|
||||
const options = {
|
||||
throwOnError: false
|
||||
throwOnError: false,
|
||||
breaks: true
|
||||
};
|
||||
|
||||
marked.use(markedKatexExtension(options));
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import UserMessage from './UserMessage.svelte';
|
||||
|
||||
export let chatId;
|
||||
export let selectedModels = [];
|
||||
export let idx = 0;
|
||||
|
||||
export let history;
|
||||
@@ -70,6 +71,7 @@
|
||||
{chatId}
|
||||
{history}
|
||||
{messageId}
|
||||
{selectedModels}
|
||||
isLastMessage={messageId === history.currentId}
|
||||
siblings={history.messages[history.messages[messageId].parentId]?.childrenIds ?? []}
|
||||
{gotoMessage}
|
||||
@@ -92,6 +94,7 @@
|
||||
bind:history
|
||||
{chatId}
|
||||
{messageId}
|
||||
{selectedModels}
|
||||
isLastMessage={messageId === history?.currentId}
|
||||
{updateChat}
|
||||
{editMessage}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let chatId;
|
||||
export let history;
|
||||
export let messageId;
|
||||
export let selectedModels = [];
|
||||
|
||||
export let isLastMessage;
|
||||
export let readOnly = false;
|
||||
@@ -252,6 +253,7 @@
|
||||
{chatId}
|
||||
{history}
|
||||
messageId={_messageId}
|
||||
{selectedModels}
|
||||
isLastMessage={true}
|
||||
siblings={groupedMessageIds[modelIdx].messageIds}
|
||||
gotoMessage={(message, messageIdx) => gotoMessage(modelIdx, messageIdx)}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { createEventDispatcher, onMount, getContext } from 'svelte';
|
||||
import { config, models } from '$lib/stores';
|
||||
import Tags from '$lib/components/common/Tags.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@@ -123,16 +124,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="size-4"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
export let chatId = '';
|
||||
export let history;
|
||||
export let messageId;
|
||||
export let selectedModels = [];
|
||||
|
||||
let message: MessageType = JSON.parse(JSON.stringify(history.messages[messageId]));
|
||||
$: if (history.messages) {
|
||||
@@ -601,7 +602,7 @@
|
||||
id="message-{message.id}"
|
||||
dir={$settings.chatDirection}
|
||||
>
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex `}>
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex mt-1 `}>
|
||||
<ProfileImage
|
||||
src={model?.info?.meta?.profile_image_url ??
|
||||
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
@@ -609,7 +610,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-auto w-0 pl-1 relative -translate-y-0.5">
|
||||
<div class="flex-auto w-0 pl-1 relative">
|
||||
<Name>
|
||||
<Tooltip content={model?.name ?? message.model} placement="top-start">
|
||||
<span class="line-clamp-1 text-black dark:text-white">
|
||||
@@ -795,6 +796,7 @@
|
||||
<ContentRenderer
|
||||
id={message.id}
|
||||
{history}
|
||||
{selectedModels}
|
||||
content={message.content}
|
||||
sources={message.sources}
|
||||
floatingButtons={message?.done && !readOnly}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
|
||||
import MagnifyingGlass from '$lib/components/icons/MagnifyingGlass.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
import Collapsible from '$lib/components/common/Collapsible.svelte';
|
||||
|
||||
export let status = { urls: [], query: '' };
|
||||
@@ -31,7 +31,7 @@
|
||||
class="flex w-full items-center p-3 px-4 border-b border-gray-300/30 dark:border-gray-700/50 group/item justify-between font-normal text-gray-800 dark:text-gray-300 no-underline"
|
||||
>
|
||||
<div class="flex gap-2 items-center">
|
||||
<MagnifyingGlass />
|
||||
<Search />
|
||||
|
||||
<div class=" line-clamp-1">
|
||||
{status.query}
|
||||
|
||||
@@ -2,44 +2,29 @@
|
||||
export let size = 'md';
|
||||
</script>
|
||||
|
||||
<div class="w-full mt-2 mb-2">
|
||||
<div class="animate-pulse flex w-full">
|
||||
<div class="{size === 'md' ? 'space-y-2' : 'space-y-1.5'} w-full">
|
||||
<div
|
||||
class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm mr-14"
|
||||
/>
|
||||
<span class="relative flex {size === 'md' ? 'size-3 my-2' : 'size-2 my-1'} mx-1">
|
||||
<span
|
||||
class="absolute inline-flex h-full w-full animate-pulse rounded-full bg-gray-700 dark:bg-gray-200 opacity-75"
|
||||
></span>
|
||||
<span
|
||||
class="relative inline-flex {size === 'md'
|
||||
? 'size-3'
|
||||
: 'size-2'} rounded-full bg-black dark:bg-white animate-size"
|
||||
></span>
|
||||
</span>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-2"
|
||||
/>
|
||||
<div
|
||||
class="{size === 'md'
|
||||
? 'h-2'
|
||||
: 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm col-span-1 mr-4"
|
||||
/>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes size {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.25);
|
||||
}
|
||||
}
|
||||
|
||||
<div class="{size === 'md' ? 'h-2' : 'h-1.5'} bg-gray-200 dark:bg-gray-600 rounded-sm" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
.animate-size {
|
||||
animation: size 1.5s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
id="message-{message.id}"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true)}
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
|
||||
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 mt-1`}>
|
||||
<ProfileImage
|
||||
src={message.user
|
||||
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
{#each selectedModels as selectedModel, selectedModelIdx}
|
||||
<div class="flex w-full max-w-fit">
|
||||
<div class="overflow-hidden w-full">
|
||||
<div class="mr-1 max-w-full">
|
||||
<div class="max-w-full {($settings?.highContrastMode ?? false) ? 'm-1' : 'mr-1'}">
|
||||
<Selector
|
||||
id={`${selectedModelIdx}`}
|
||||
placeholder={$i18n.t('Select a model')}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -345,12 +346,17 @@
|
||||
closeFocus={false}
|
||||
>
|
||||
<DropdownMenu.Trigger
|
||||
class="relative w-full font-primary"
|
||||
class="relative w-full font-primary {($settings?.highContrastMode ?? false)
|
||||
? ''
|
||||
: 'outline-hidden focus:outline-hidden'}"
|
||||
aria-label={placeholder}
|
||||
id="model-selector-{id}-button"
|
||||
>
|
||||
<button
|
||||
class="flex w-full text-left px-0.5 outline-hidden bg-transparent truncate {triggerClassName} justify-between font-medium placeholder-gray-400 focus:outline-hidden"
|
||||
<div
|
||||
class="flex w-full text-left px-0.5 bg-transparent truncate {triggerClassName} justify-between {($settings?.highContrastMode ??
|
||||
false)
|
||||
? 'dark:placeholder-gray-100 placeholder-gray-800'
|
||||
: 'placeholder-gray-400'}"
|
||||
on:mouseenter={async () => {
|
||||
models.set(
|
||||
await getModels(
|
||||
@@ -359,7 +365,6 @@
|
||||
)
|
||||
);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{#if selectedModel}
|
||||
{selectedModel.label}
|
||||
@@ -367,7 +372,7 @@
|
||||
{placeholder}
|
||||
{/if}
|
||||
<ChevronDown className=" self-center ml-2 size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
</div>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Content
|
||||
@@ -550,29 +555,7 @@
|
||||
>
|
||||
<div class="flex">
|
||||
<div class="-ml-2 mr-2.5 translate-y-0.5">
|
||||
<svg
|
||||
class="size-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col self-start">
|
||||
|
||||
@@ -173,19 +173,19 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<button
|
||||
<div
|
||||
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-50 dark:hover:bg-gray-850 transition"
|
||||
aria-label="User Menu"
|
||||
>
|
||||
<div class=" self-center">
|
||||
<span class="sr-only">{$i18n.t('User menu')}</span>
|
||||
<img
|
||||
src={$user?.profile_image_url}
|
||||
class="size-6 object-cover rounded-full"
|
||||
alt="User profile"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</UserMenu>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -118,6 +118,8 @@
|
||||
placement="top"
|
||||
>
|
||||
<button
|
||||
aria-hidden={models.length <= 1}
|
||||
aria-label={$i18n.t('Get information on {{name}} in the UI', { name: models[modelIdx]?.name})}
|
||||
on:click={() => {
|
||||
selectedModelIdx = modelIdx;
|
||||
}}
|
||||
@@ -129,7 +131,7 @@
|
||||
? `/doge.png`
|
||||
: `${WEBUI_BASE_URL}/static/favicon.png`)}
|
||||
class=" size-9 @sm:size-10 rounded-full border-[1px] border-gray-100 dark:border-none"
|
||||
alt="logo"
|
||||
aria-hidden="true"
|
||||
draggable="false"
|
||||
/>
|
||||
</button>
|
||||
@@ -164,7 +166,9 @@
|
||||
<Tooltip
|
||||
className=" w-fit"
|
||||
content={marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description ?? '')
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description ?? ''
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
@@ -172,7 +176,9 @@
|
||||
class="mt-0.5 px-2 text-sm font-normal text-gray-500 dark:text-gray-400 line-clamp-2 max-w-xl markdown"
|
||||
>
|
||||
{@html marked.parse(
|
||||
sanitizeResponseContent(models[selectedModelIdx]?.info?.meta?.description)
|
||||
sanitizeResponseContent(
|
||||
models[selectedModelIdx]?.info?.meta?.description ?? ''
|
||||
).replaceAll('\n', '<br>')
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
@@ -218,9 +224,9 @@
|
||||
onChange={(input) => {
|
||||
if (!$temporaryChatEnabled) {
|
||||
if (input.prompt !== null) {
|
||||
localStorage.setItem(`chat-input`, JSON.stringify(input));
|
||||
sessionStorage.setItem(`chat-input`, JSON.stringify(input));
|
||||
} else {
|
||||
localStorage.removeItem(`chat-input`);
|
||||
sessionStorage.removeItem(`chat-input`);
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -38,7 +38,9 @@
|
||||
return '';
|
||||
});
|
||||
|
||||
checkForVersionUpdates();
|
||||
if (!$config?.offline_mode) {
|
||||
checkForVersionUpdates();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -80,14 +82,16 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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-medium"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{#if $config?.offline_mode}
|
||||
<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-medium"
|
||||
on:click={() => {
|
||||
checkForVersionUpdates();
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Check for updates')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
|
||||
import {
|
||||
archiveAllChats,
|
||||
createNewChat,
|
||||
deleteAllChats,
|
||||
getAllChats,
|
||||
getAllUserChats,
|
||||
getChatList
|
||||
getChatList,
|
||||
importChat
|
||||
} from '$lib/apis/chats';
|
||||
import { getImportOrigin, convertOpenAIChats } from '$lib/utils';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
@@ -58,9 +57,18 @@
|
||||
console.log(chat);
|
||||
|
||||
if (chat.chat) {
|
||||
await createNewChat(localStorage.token, chat.chat);
|
||||
await importChat(
|
||||
localStorage.token,
|
||||
chat.chat,
|
||||
chat.meta ?? {},
|
||||
false,
|
||||
null,
|
||||
chat?.created_at ?? null,
|
||||
chat?.updated_at ?? null
|
||||
);
|
||||
} else {
|
||||
await createNewChat(localStorage.token, chat);
|
||||
// Legacy format
|
||||
await importChat(localStorage.token, chat, {}, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +109,7 @@
|
||||
const handleArchivedChatsChange = async () => {
|
||||
currentChatPage.set(1);
|
||||
await chats.set(await getChatList(localStorage.token, $currentChatPage));
|
||||
|
||||
scrollPaginationEnabled.set(true);
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -274,7 +274,10 @@
|
||||
<div class=" my-2.5 text-sm font-medium">{$i18n.t('System Prompt')}</div>
|
||||
<Textarea
|
||||
bind:value={system}
|
||||
className="w-full text-sm bg-white dark:text-gray-300 dark:bg-gray-900 outline-hidden resize-none"
|
||||
className={'w-full text-sm outline-hidden resize-vertical' +
|
||||
($settings.highContrastMode
|
||||
? ' p-2.5 border-2 border-gray-300 dark:border-gray-700 rounded-lg bg-gray-50 dark:bg-gray-850 text-gray-900 dark:text-gray-100 focus:ring-1 focus:ring-blue-500 focus:border-blue-500 overflow-y-hidden'
|
||||
: ' bg-white dark:text-gray-300 dark:bg-gray-900')}
|
||||
rows="4"
|
||||
placeholder={$i18n.t('Enter system prompt here')}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import { addNewMemory, updateMemoryById } from '$lib/apis/memories';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -46,16 +48,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -93,29 +86,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
|
||||
import { updateMemoryById } from '$lib/apis/memories';
|
||||
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Modal from '$lib/components/common/Modal.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -56,16 +58,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -103,29 +96,7 @@
|
||||
|
||||
{#if loading}
|
||||
<div class="ml-2 self-center">
|
||||
<svg
|
||||
class=" w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
animation: spinner_AtaB 0.75s infinite linear;
|
||||
}
|
||||
@keyframes spinner_AtaB {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style><path
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
/><path
|
||||
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||
class="spinner_ajPY"
|
||||
/></svg
|
||||
>
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
@@ -211,9 +211,7 @@
|
||||
: []),
|
||||
|
||||
...($user?.role === 'admin' ||
|
||||
($user?.role === 'user' &&
|
||||
$user?.permissions?.features?.direct_tool_servers &&
|
||||
$config?.features?.direct_tool_servers)
|
||||
($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)
|
||||
? [
|
||||
{
|
||||
id: 'tools',
|
||||
@@ -688,7 +686,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
{:else if tabId === 'tools'}
|
||||
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers && $config?.features?.direct_tool_servers)}
|
||||
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)}
|
||||
<button
|
||||
role="tab"
|
||||
aria-controls="tab-tools"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Link from '../icons/Link.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let chatId;
|
||||
|
||||
@@ -90,16 +91,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
const i18n = getContext('i18n');
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
</script>
|
||||
@@ -18,16 +19,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -83,9 +83,10 @@
|
||||
|
||||
<div class="h-40 w-full">
|
||||
{#if filteredPrompts.length > 0}
|
||||
<div class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
||||
<div role="list" class="max-h-40 overflow-auto scrollbar-none items-start {className}">
|
||||
{#each filteredPrompts as prompt, idx (prompt.id || prompt.content)}
|
||||
<button
|
||||
role="listitem"
|
||||
class="waterfall flex flex-col flex-1 shrink-0 w-full justify-between
|
||||
px-3 py-2 rounded-xl bg-transparent hover:bg-black/5
|
||||
dark:hover:bg-white/5 transition group"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Modal from '../common/Modal.svelte';
|
||||
import Link from '../icons/Link.svelte';
|
||||
import Collapsible from '../common/Collapsible.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let selectedToolIds = [];
|
||||
@@ -30,16 +31,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="w-5 h-5"
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-5'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
|
||||
console.log('Banner mounted:', banner);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -82,9 +84,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-white max-h-20 overflow-y-auto">
|
||||
{@html marked.parse(DOMPurify.sanitize(banner.content))}
|
||||
<div class="flex-1 text-xs text-gray-700 dark:text-white max-h-60 overflow-y-auto">
|
||||
{@html marked.parse(DOMPurify.sanitize((banner?.content ?? '').replace(/\n/g, '<br>')))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,35 +48,28 @@
|
||||
/**
|
||||
* Finds multiple diffs in two strings and generates minimal change edits.
|
||||
*/
|
||||
function findChanges(oldStr, newStr) {
|
||||
let changes = [];
|
||||
let oldIndex = 0,
|
||||
newIndex = 0;
|
||||
|
||||
while (oldIndex < oldStr.length || newIndex < newStr.length) {
|
||||
if (oldStr[oldIndex] !== newStr[newIndex]) {
|
||||
let start = oldIndex;
|
||||
|
||||
// Identify the changed portion
|
||||
while (oldIndex < oldStr.length && oldStr[oldIndex] !== newStr[newIndex]) {
|
||||
oldIndex++;
|
||||
}
|
||||
while (newIndex < newStr.length && newStr[newIndex] !== oldStr[start]) {
|
||||
newIndex++;
|
||||
}
|
||||
|
||||
changes.push({
|
||||
from: start,
|
||||
to: oldIndex, // Replace the differing part
|
||||
insert: newStr.substring(start, newIndex)
|
||||
});
|
||||
} else {
|
||||
oldIndex++;
|
||||
newIndex++;
|
||||
}
|
||||
function findChanges(oldStr: string, newStr: string) {
|
||||
// Find the start of the difference
|
||||
let start = 0;
|
||||
while (start < oldStr.length && start < newStr.length && oldStr[start] === newStr[start]) {
|
||||
start++;
|
||||
}
|
||||
|
||||
return changes;
|
||||
// If equal, nothing to change
|
||||
if (oldStr === newStr) return [];
|
||||
// Find the end of the difference by comparing backwards
|
||||
let endOld = oldStr.length,
|
||||
endNew = newStr.length;
|
||||
while (endOld > start && endNew > start && oldStr[endOld - 1] === newStr[endNew - 1]) {
|
||||
endOld--;
|
||||
endNew--;
|
||||
}
|
||||
return [
|
||||
{
|
||||
from: start,
|
||||
to: endOld,
|
||||
insert: newStr.slice(start, endNew)
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export let id = '';
|
||||
|
||||
@@ -25,11 +25,19 @@
|
||||
|
||||
export let show = false;
|
||||
|
||||
$: if (show) {
|
||||
init();
|
||||
}
|
||||
|
||||
let modalElement = null;
|
||||
let mounted = false;
|
||||
|
||||
let focusTrap: FocusTrap.FocusTrap | null = null;
|
||||
|
||||
const init = () => {
|
||||
inputValue = '';
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
console.log('Escape');
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import GarbageBin from '../icons/GarbageBin.svelte';
|
||||
import Spinner from './Spinner.svelte';
|
||||
import Tooltip from './Tooltip.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { settings } from '$lib/stores';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const dispatch = createEventDispatcher();
|
||||
@@ -70,6 +72,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
class=" size-5"
|
||||
>
|
||||
<path
|
||||
@@ -93,7 +96,11 @@
|
||||
{decodeString(name)}
|
||||
</div>
|
||||
|
||||
<div class=" flex justify-between text-gray-500 text-xs line-clamp-1">
|
||||
<div
|
||||
class=" flex justify-between text-xs line-clamp-1 {($settings?.highContrastMode ?? false)
|
||||
? 'text-gray-800 dark:text-gray-100'
|
||||
: 'text-gray-500'}"
|
||||
>
|
||||
{#if type === 'file'}
|
||||
{$i18n.t('File')}
|
||||
{:else if type === 'doc'}
|
||||
@@ -127,22 +134,17 @@
|
||||
{#if dismissible}
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
class=" bg-white text-black border border-gray-50 rounded-full group-hover:visible invisible transition"
|
||||
aria-label={$i18n.t('Remove File')}
|
||||
class=" bg-white text-black border border-gray-50 rounded-full {($settings?.highContrastMode ??
|
||||
false)
|
||||
? ''
|
||||
: 'outline-hidden focus:outline-hidden group-hover:visible invisible transition'}"
|
||||
type="button"
|
||||
on:click|stopPropagation={() => {
|
||||
dispatch('dismiss');
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
|
||||
<!-- <button
|
||||
|
||||
@@ -69,12 +69,22 @@
|
||||
}
|
||||
} else {
|
||||
open = true;
|
||||
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
const data = JSON.parse(dataTransfer);
|
||||
|
||||
console.log(data);
|
||||
dispatch('drop', data);
|
||||
try {
|
||||
const dataTransfer = e.dataTransfer.getData('text/plain');
|
||||
if (dataTransfer) {
|
||||
const data = JSON.parse(dataTransfer);
|
||||
console.log(data);
|
||||
dispatch('drop', data);
|
||||
} else {
|
||||
console.log('Dropped text data is empty or not text/plain.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'Dropped data is not valid JSON text or is empty. Ignoring drop event for this type of data.'
|
||||
);
|
||||
} finally {
|
||||
draggedOver = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,7 +113,7 @@
|
||||
if (!dragAndDrop) {
|
||||
return;
|
||||
}
|
||||
folderElement.addEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('dragover', onDragOver);
|
||||
folderElement.removeEventListener('drop', onDrop);
|
||||
folderElement.removeEventListener('dragleave', onDragLeave);
|
||||
});
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { WEBUI_BASE_URL } from '$lib/constants';
|
||||
|
||||
import { settings } from '$lib/stores';
|
||||
import ImagePreview from './ImagePreview.svelte';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
export let src = '';
|
||||
export let alt = '';
|
||||
|
||||
export let className = ' w-full outline-hidden focus:outline-hidden';
|
||||
export let className = ` w-full ${($settings?.highContrastMode ?? false) ? '' : 'outline-hidden focus:outline-hidden'}`;
|
||||
|
||||
export let imageClassName = 'rounded-lg';
|
||||
|
||||
export let dismissible = false;
|
||||
export let onDismiss = () => {};
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let _src = '';
|
||||
$: _src = src.startsWith('/') ? `${WEBUI_BASE_URL}${src}` : src;
|
||||
|
||||
@@ -25,6 +32,7 @@
|
||||
on:click={() => {
|
||||
showImagePreview = true;
|
||||
}}
|
||||
aria-label={$i18n.t('Show image preview')}
|
||||
type="button"
|
||||
>
|
||||
<img src={_src} {alt} class={imageClassName} draggable="false" data-cy="image" />
|
||||
@@ -33,22 +41,14 @@
|
||||
{#if dismissible}
|
||||
<div class=" absolute -top-1 -right-1">
|
||||
<button
|
||||
aria-label={$i18n.t('Remove image')}
|
||||
class=" bg-white text-black border border-white rounded-full group-hover:visible invisible transition"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
onDismiss();
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
class="size-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>
|
||||
<XMark className={'size-4'} />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
import fileSaver from 'file-saver';
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
|
||||
export let show = false;
|
||||
export let src = '';
|
||||
export let alt = '';
|
||||
@@ -82,16 +84,7 @@
|
||||
show = false;
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-6 h-6"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<XMark className={'size-6'} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
{#if show}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div
|
||||
bind:this={modalElement}
|
||||
aria-modal="true"
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
export let required = true;
|
||||
export let readOnly = false;
|
||||
export let outerClassName = 'flex flex-1 bg-transparent';
|
||||
export let inputClassName =
|
||||
'w-full text-sm py-0.5 placeholder:text-gray-300 dark:placeholder:text-gray-700 bg-transparent outline-hidden';
|
||||
export let inputClassName = 'w-full text-sm py-0.5 bg-transparent';
|
||||
export let showButtonClassName = 'pl-1.5 transition bg-transparent';
|
||||
|
||||
let show = false;
|
||||
@@ -17,7 +16,7 @@
|
||||
<div class={outerClassName}>
|
||||
<label class="sr-only" for="password-input">{placeholder || $i18n.t('Password')}</label>
|
||||
<input
|
||||
class={`${inputClassName} ${show ? '' : 'password'} ${($settings?.highContrastMode ?? false) ? '' : ' outline-hidden'}`}
|
||||
class={`${inputClassName} ${show ? '' : 'password'} ${($settings?.highContrastMode ?? false) ? 'placeholder:text-gray-700 dark:placeholder:text-gray-100' : ' outline-hidden placeholder:text-gray-300 dark:placeholder:text-gray-700'}`}
|
||||
{placeholder}
|
||||
id="password-input"
|
||||
bind:value
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let className: string = 'size-5';
|
||||
export let className: string = 'size-4';
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center text-center">
|
||||
<svg class={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class={className}
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><style>
|
||||
.spinner_ajPY {
|
||||
transform-origin: center;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
import { Switch } from 'bits-ui';
|
||||
import { settings } from '$lib/stores';
|
||||
export let state = true;
|
||||
export let id = '';
|
||||
|
||||
@@ -12,9 +13,12 @@
|
||||
<Switch.Root
|
||||
bind:checked={state}
|
||||
{id}
|
||||
class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition {state
|
||||
class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] mx-[1px] transition {($settings?.highContrastMode ??
|
||||
false)
|
||||
? 'focus:outline focus:outline-2 focus:outline-gray-800 focus:dark:outline-gray-200'
|
||||
: 'outline outline-1 outline-gray-100 dark:outline-gray-800'} {state
|
||||
? ' bg-emerald-600'
|
||||
: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
|
||||
: 'bg-gray-200 dark:bg-transparent'}"
|
||||
>
|
||||
<Switch.Thumb
|
||||
class="pointer-events-none block size-4 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini "
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let placeholder = '';
|
||||
export let rows = 1;
|
||||
export let minSize = null;
|
||||
export let maxSize = null;
|
||||
export let required = false;
|
||||
export let className =
|
||||
'w-full rounded-lg px-3.5 py-2 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden h-full';
|
||||
@@ -30,9 +31,16 @@
|
||||
const resize = () => {
|
||||
if (textareaElement) {
|
||||
textareaElement.style.height = '';
|
||||
textareaElement.style.height = minSize
|
||||
? `${Math.max(textareaElement.scrollHeight, minSize)}px`
|
||||
: `${textareaElement.scrollHeight}px`;
|
||||
|
||||
let height = textareaElement.scrollHeight;
|
||||
if (maxSize && height > maxSize) {
|
||||
height = maxSize;
|
||||
}
|
||||
if (minSize && height < minSize) {
|
||||
height = minSize;
|
||||
}
|
||||
|
||||
textareaElement.style.height = `${height}px`;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import Switch from './Switch.svelte';
|
||||
import MapSelector from './Valves/MapSelector.svelte';
|
||||
import { split } from 'postcss/lib/list';
|
||||
|
||||
export let valvesSpec = null;
|
||||
export let valves = {};
|
||||
@@ -49,7 +51,7 @@
|
||||
|
||||
{#if (valves[property] ?? null) !== null}
|
||||
<!-- {valves[property]} -->
|
||||
<div class="flex mt-0.5 mb-1.5 space-x-2">
|
||||
<div class="flex mt-0.5 mb-0.5 space-x-2">
|
||||
<div class=" flex-1">
|
||||
{#if valvesSpec.properties[property]?.enum ?? null}
|
||||
<select
|
||||
@@ -92,6 +94,61 @@
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{:else if valvesSpec.properties[property]?.input ?? null}
|
||||
{#if valvesSpec.properties[property]?.input?.type === 'color'}
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="relative size-6">
|
||||
<input
|
||||
type="color"
|
||||
class="size-6 rounded cursor-pointer border border-gray-200 dark:border-gray-700"
|
||||
value={valves[property] ?? '#000000'}
|
||||
on:input={(e) => {
|
||||
// Convert the color value to uppercase immediately
|
||||
valves[property] = e.target.value.toUpperCase();
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="text"
|
||||
class="flex-1 rounded-lg py-2 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder="Enter hex color (e.g. #FF0000)"
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
disabled
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if valvesSpec.properties[property]?.input?.type === 'map'}
|
||||
<!-- EXPERIMENTAL INPUT TYPE, DO NOT USE IN PRODUCTION -->
|
||||
<div class="flex flex-col items-center gap-1">
|
||||
<MapSelector
|
||||
setViewLocation={((valves[property] ?? '').includes(',') ?? false)
|
||||
? valves[property].split(',')
|
||||
: null}
|
||||
onClick={(value) => {
|
||||
valves[property] = value;
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if valves[property]}
|
||||
<input
|
||||
type="text"
|
||||
class=" w-full rounded-lg py-1 text-left text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
placeholder="Enter coordinates (e.g. 51.505, -0.09)"
|
||||
bind:value={valves[property]}
|
||||
autocomplete="off"
|
||||
on:change={() => {
|
||||
dispatch('change');
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<textarea
|
||||
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden border border-gray-100 dark:border-gray-850"
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
<script>
|
||||
import L from 'leaflet';
|
||||
import 'leaflet/dist/leaflet.css';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
|
||||
let map;
|
||||
let mapElement;
|
||||
|
||||
export let setViewLocation = [51.505, -0.09];
|
||||
export let points = [];
|
||||
|
||||
export let onClick = (e) => {};
|
||||
|
||||
let markerGroupLayer = null;
|
||||
|
||||
const setMarkers = (points) => {
|
||||
if (map) {
|
||||
if (markerGroupLayer) {
|
||||
map.removeLayer(markerGroupLayer);
|
||||
}
|
||||
|
||||
let markers = [];
|
||||
for (let point of points) {
|
||||
const marker = L.marker(point.coords).bindPopup(point.content);
|
||||
|
||||
markers.push(marker);
|
||||
}
|
||||
|
||||
markerGroupLayer = L.featureGroup(markers).addTo(map);
|
||||
|
||||
try {
|
||||
map.fitBounds(markerGroupLayer.getBounds(), {
|
||||
maxZoom: 13
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fitting bounds for markers:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
map = L.map(mapElement).setView(setViewLocation ? setViewLocation : [51.505, -0.09], 10);
|
||||
|
||||
if (setViewLocation) {
|
||||
points = [
|
||||
{
|
||||
coords: setViewLocation,
|
||||
content: `Lat: ${setViewLocation[0]}, Lng: ${setViewLocation[1]}`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution:
|
||||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
setMarkers(points);
|
||||
|
||||
map.on('click', (e) => {
|
||||
console.log(e.latlng);
|
||||
onClick(`${e.latlng.lat}, ${e.latlng.lng}`);
|
||||
|
||||
setMarkers([
|
||||
{
|
||||
coords: [e.latlng.lat, e.latlng.lng],
|
||||
content: `Lat: ${e.latlng.lat}, Lng: ${e.latlng.lng}`
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(async () => {
|
||||
if (map) {
|
||||
console.log('Unloading Leaflet map.');
|
||||
map.remove();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class=" z-10 w-full">
|
||||
<div bind:this={mapElement} class="h-96 z-10" />
|
||||
</div>
|
||||
@@ -9,6 +9,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let className = 'size-4';
|
||||
export let strokeWidth = '2';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -7,6 +7,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let className: string = 'w-5 h-5';
|
||||
export let strokeWidth: string = '1.5';
|
||||
</script>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
|
||||
@@ -7,6 +8,7 @@
|
||||
fill-rule="evenodd"
|
||||
d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
|
||||
clip-rule="evenodd"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
aria-hidden="true"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
aria-hidden="true"
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user