Merge branch 'dev' into update-ga-strings-june

This commit is contained in:
Aindriú Mac Giolla Eoin
2025-07-03 13:07:38 +01:00
committed by GitHub
226 changed files with 5101 additions and 3210 deletions
+2
View File
@@ -347,6 +347,8 @@ export const userSignOut = async () => {
if (error) {
throw error;
}
sessionStorage.clear();
return res;
};
+6 -2
View File
@@ -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) => {
+4 -4
View File
@@ -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
View File
@@ -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;
+1
View File
@@ -403,6 +403,7 @@ export const deleteUserById = async (token: string, userId: string) => {
};
type UserUpdateForm = {
role: string;
profile_image_url: string;
email: string;
name: string;
+4 -34
View File
@@ -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>
+49 -47
View File
@@ -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>
+4 -10
View File
@@ -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">
+4 -33
View File
@@ -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"
+15 -5
View File
@@ -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
)
);
};
+2 -27
View File
@@ -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">
+2 -12
View File
@@ -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>
+46 -34
View File
@@ -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);
}
+11 -10
View 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')}
/>
+61 -38
View File
@@ -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>
+1
View File
@@ -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 ??
+1 -1
View File
@@ -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">
+4 -4
View File
@@ -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>
+11 -5
View File
@@ -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`);
}
}
}}
+13 -9
View File
@@ -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>
+14 -5
View File
@@ -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>
+2 -4
View File
@@ -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"
+2 -10
View File
@@ -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>
+2 -10
View File
@@ -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>
+2 -1
View File
@@ -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>
+4 -3
View File
@@ -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>
+21 -28
View File
@@ -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');
+14 -12
View File
@@ -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
+17 -7
View File
@@ -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);
});
+11 -11
View File
@@ -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}
+3 -10
View File
@@ -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>
+1
View File
@@ -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
+7 -2
View File
@@ -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;
+6 -2
View File
@@ -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 "
+11 -3
View File
@@ -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>
+58 -1
View File
@@ -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:
'&copy; <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
+1
View File
@@ -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>
+1
View File
@@ -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"
+1
View File
@@ -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>
+1
View File
@@ -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
+1
View File
@@ -9,6 +9,7 @@
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
aria-hidden="true"
class={className}
>
<path
+2
View File
@@ -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"
+1
View File
@@ -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}
+1
View File
@@ -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