Merge branch 'open-webui:main' into correct-markdown-rendering-model-descriptions

This commit is contained in:
Classic298
2025-06-16 21:11:18 +02:00
committed by GitHub
153 changed files with 8216 additions and 3021 deletions
+71 -20
View File
@@ -3,7 +3,7 @@
import { getContext, onMount } from 'svelte';
const i18n = getContext('i18n');
import { models } from '$lib/stores';
import { settings } from '$lib/stores';
import { verifyOpenAIConnection } from '$lib/apis/openai';
import { verifyOllamaConnection } from '$lib/apis/ollama';
@@ -194,15 +194,16 @@
<Modal size="sm" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-100 px-5 pt-4 pb-1.5">
<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 modal')}
on:click={() => {
show = false;
}}
@@ -211,6 +212,7 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
class="w-5 h-5"
>
<path
@@ -256,11 +258,17 @@
<div class="flex gap-2 mt-1.5">
<div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('URL')}</div>
<label
for="url-input"
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>{$i18n.t('URL')}</label
>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
id="url-input"
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={url}
placeholder={$i18n.t('API Base URL')}
@@ -277,11 +285,13 @@
verifyHandler();
}}
type="button"
aria-label={$i18n.t('Verify Connection')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
class="w-4 h-4"
>
<path
@@ -294,19 +304,27 @@
</Tooltip>
<div class="flex flex-col shrink-0 self-end">
<label class="sr-only" for="toggle-connection"
>{$i18n.t('Toggle whether current connection is active.')}</label
>
<Tooltip content={enable ? $i18n.t('Enabled') : $i18n.t('Disabled')}>
<Switch bind:state={enable} />
<Switch id="toggle-connection" bind:state={enable} />
</Tooltip>
</div>
</div>
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class=" mb-0.5 text-xs text-gray-500">{$i18n.t('Key')}</div>
<div
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>
{$i18n.t('Key')}
</div>
<div class="flex-1">
<SensitiveInput
className="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
inputClassName={`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'}`}
bind:value={key}
placeholder={$i18n.t('API Key')}
required={false}
@@ -315,7 +333,12 @@
</div>
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('Prefix ID')}</div>
<label
for="prefix-id-input"
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>{$i18n.t('Prefix ID')}</label
>
<div class="flex-1">
<Tooltip
@@ -324,8 +347,9 @@
)}
>
<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="prefix-id-input"
bind:value={prefixId}
placeholder={$i18n.t('Prefix ID')}
autocomplete="off"
@@ -338,11 +362,17 @@
{#if azure}
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class=" mb-1 text-xs text-gray-500">{$i18n.t('API Version')}</div>
<label
for="api-version-input"
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>{$i18n.t('API Version')}</label
>
<div class="flex-1">
<input
class="w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
id="api-version-input"
class={`w-full text-sm bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-700 ${($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={apiVersion}
placeholder={$i18n.t('API Version')}
@@ -356,7 +386,12 @@
<div class="flex gap-2 mt-2">
<div class="flex flex-col w-full">
<div class=" mb-1.5 text-xs text-gray-500">{$i18n.t('Tags')}</div>
<div
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>
{$i18n.t('Tags')}
</div>
<div class="flex-1">
<Tags
@@ -381,18 +416,26 @@
<div class="flex flex-col w-full">
<div class="mb-1 flex justify-between">
<div class="text-xs text-gray-500">{$i18n.t('Model IDs')}</div>
<div
class={`mb-0.5 text-xs text-gray-500
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>
{$i18n.t('Model IDs')}
</div>
</div>
{#if modelIds.length > 0}
<div class="flex flex-col">
<ul class="flex flex-col">
{#each modelIds as modelId, modelIdx}
<div class=" flex gap-2 w-full justify-between items-center">
<li class=" flex gap-2 w-full justify-between items-center">
<div class=" text-sm flex-1 py-1 rounded-lg">
{modelId}
</div>
<div class="shrink-0">
<button
aria-label={$i18n.t(`Remove {{MODELID}} from list.`, {
MODELID: modelId
})}
type="button"
on:click={() => {
modelIds = modelIds.filter((_, idx) => idx !== modelIdx);
@@ -401,11 +444,14 @@
<Minus strokeWidth="2" className="size-3.5" />
</button>
</div>
</div>
</li>
{/each}
</div>
</ul>
{:else}
<div class="text-gray-500 text-xs text-center py-2 px-10">
<div
class={`text-gray-500 text-xs text-center py-2 px-10
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : ''}`}
>
{#if ollama}
{$i18n.t('Leave empty to include all models from "{{url}}/api/tags" endpoint', {
url: url
@@ -427,17 +473,22 @@
<hr class=" border-gray-100 dark:border-gray-700/10 my-1.5 w-full" />
<div class="flex items-center">
<label class="sr-only" for="add-model-id-input">{$i18n.t('Add a model ID')}</label>
<input
class="w-full py-1 text-sm rounded-lg bg-transparent {modelId
? ''
: 'text-gray-500'} placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden"
: 'text-gray-500'} {($settings?.highContrastMode ?? false)
? 'dark:placeholder:text-gray-100 placeholder:text-gray-700'
: 'placeholder:text-gray-300 dark:placeholder:text-gray-700 outline-hidden'}"
bind:value={modelId}
id="add-model-id-input"
placeholder={$i18n.t('Add a model ID')}
/>
<div>
<button
type="button"
aria-label={$i18n.t('Add')}
on:click={() => {
addModelHandler();
}}
+1 -1
View File
@@ -26,7 +26,7 @@
<div class="px-5 pt-4 dark:text-gray-300 text-gray-700">
<div class="flex justify-between items-start">
<div class="text-xl font-semibold">
{$i18n.t('What’s New in')}
{$i18n.t("What's New in")}
{$WEBUI_NAME}
<Confetti x={[-1, -0.25]} y={[0, 0.5]} />
</div>
@@ -0,0 +1,80 @@
<script lang="ts">
import Modal from '$lib/components/common/Modal.svelte';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
export let show = false;
export let selectedFeedback = null;
export let onClose: () => void = () => {};
const close = () => {
show = false;
onClose();
};
</script>
<Modal size="sm" bind:show>
{#if selectedFeedback}
<div>
<div class="flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class="text-lg font-medium self-center">
{$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>
</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>
<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>
<div class="flex-1">
<span>{selectedFeedback?.data?.reason || '-'}</span>
</div>
</div>
<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
>
{/each}
</div>
{:else}
<span>-</span>
{/if}
</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>
</div>
</div>
</div>
</div>
{/if}
</Modal>
@@ -18,12 +18,19 @@
import CloudArrowUp from '$lib/components/icons/CloudArrowUp.svelte';
import Pagination from '$lib/components/common/Pagination.svelte';
import FeedbackMenu from './FeedbackMenu.svelte';
import FeedbackModal from './FeedbackModal.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
export let feedbacks = [];
let page = 1;
$: paginatedFeedbacks = feedbacks.slice((page - 1) * 10, page * 10);
$: paginatedFeedbacks = sortedFeedbacks.slice((page - 1) * 10, page * 10);
let orderBy: string = 'updated_at';
let direction: 'asc' | 'desc' = 'desc';
type Feedback = {
id: string;
@@ -48,6 +55,58 @@
lost: number;
};
function setSortKey(key: string) {
if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
orderBy = key;
if (key === 'user' || key === 'model_id') {
direction = 'asc';
} else {
direction = 'desc';
}
}
page = 1;
}
$: sortedFeedbacks = [...feedbacks].sort((a, b) => {
let aVal, bVal;
switch (orderBy) {
case 'user':
aVal = a.user?.name || '';
bVal = b.user?.name || '';
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
case 'model_id':
aVal = a.data.model_id || '';
bVal = b.data.model_id || '';
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
case 'rating':
aVal = a.data.rating;
bVal = b.data.rating;
return direction === 'asc' ? aVal - bVal : bVal - aVal;
case 'updated_at':
aVal = a.updated_at;
bVal = b.updated_at;
return direction === 'asc' ? aVal - bVal : bVal - aVal;
default:
return 0;
}
});
let showFeedbackModal = false;
let selectedFeedback = null;
const openFeedbackModal = (feedback) => {
showFeedbackModal = true;
selectedFeedback = feedback;
};
const closeFeedbackModal = () => {
showFeedbackModal = false;
selectedFeedback = null;
};
//////////////////////
//
// CRUD operations
@@ -106,6 +165,8 @@
};
</script>
<FeedbackModal bind:show={showFeedbackModal} {selectedFeedback} onClose={closeFeedbackModal} />
<div class="mt-0.5 mb-2 gap-1 flex flex-row justify-between">
<div class="flex md:self-center text-lg font-medium px-0.5">
{$i18n.t('Feedback History')}
@@ -146,20 +207,96 @@
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
>
<tr class="">
<th scope="col" class="px-3 text-right cursor-pointer select-none w-0">
{$i18n.t('User')}
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none w-3"
on:click={() => setSortKey('user')}
>
<div class="flex gap-1.5 items-center justify-end">
{$i18n.t('User')}
{#if orderBy === 'user'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 pr-1.5 cursor-pointer select-none">
{$i18n.t('Models')}
<th
scope="col"
class="px-3 pr-1.5 cursor-pointer select-none"
on:click={() => setSortKey('model_id')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Models')}
{#if orderBy === 'model_id'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
{$i18n.t('Result')}
<th
scope="col"
class="px-3 py-1.5 text-right cursor-pointer select-none w-fit"
on:click={() => setSortKey('rating')}
>
<div class="flex gap-1.5 items-center justify-end">
{$i18n.t('Result')}
{#if orderBy === 'rating'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-0">
{$i18n.t('Updated At')}
<th
scope="col"
class="px-3 py-1.5 text-right cursor-pointer select-none w-0"
on:click={() => setSortKey('updated_at')}
>
<div class="flex gap-1.5 items-center justify-end">
{$i18n.t('Updated At')}
{#if orderBy === 'updated_at'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-0"> </th>
@@ -167,7 +304,10 @@
</thead>
<tbody class="">
{#each paginatedFeedbacks as feedback (feedback.id)}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs">
<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"
on:click={() => openFeedbackModal(feedback)}
>
<td class=" py-0.5 text-right font-semibold">
<div class="flex justify-center">
<Tooltip content={feedback?.user?.name}>
@@ -7,10 +7,15 @@
import { onMount, getContext } from 'svelte';
import { models } from '$lib/stores';
import ModelModal from './LeaderboardModal.svelte';
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 ChevronUp from '$lib/components/icons/ChevronUp.svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
const i18n = getContext('i18n');
const EMBEDDING_MODEL = 'TaylorAI/bge-micro-v2';
@@ -28,6 +33,9 @@
let loadingLeaderboard = true;
let debounceTimer;
let orderBy: string = 'rating'; // default sort column
let direction: 'asc' | 'desc' = 'desc'; // default sort order
type Feedback = {
id: string;
data: {
@@ -51,6 +59,34 @@
lost: number;
};
function setSortKey(key) {
if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
orderBy = key;
direction = key === 'name' ? 'asc' : 'desc';
}
}
//////////////////////
//
// Aggregate Level Modal
//
//////////////////////
let showLeaderboardModal = false;
let selectedModel = null;
const openFeedbackModal = (model) => {
showLeaderboardModal = true;
selectedModel = model;
};
const closeLeaderboardModal = () => {
showLeaderboardModal = false;
selectedModel = null;
};
//////////////////////
//
// Rank models by Elo rating
@@ -266,8 +302,37 @@
onMount(async () => {
rankHandler();
});
$: sortedModels = [...rankedModels].sort((a, b) => {
let aVal, bVal;
if (orderBy === 'name') {
aVal = a.name;
bVal = b.name;
return direction === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
} else if (orderBy === 'rating') {
aVal = a.rating === '-' ? -Infinity : a.rating;
bVal = b.rating === '-' ? -Infinity : b.rating;
return direction === 'asc' ? aVal - bVal : bVal - aVal;
} else if (orderBy === 'won') {
aVal = a.stats.won === '-' ? -Infinity : Number(a.stats.won);
bVal = b.stats.won === '-' ? -Infinity : Number(b.stats.won);
return direction === 'asc' ? aVal - bVal : bVal - aVal;
} else if (orderBy === 'lost') {
aVal = a.stats.lost === '-' ? -Infinity : Number(a.stats.lost);
bVal = b.stats.lost === '-' ? -Infinity : Number(b.stats.lost);
return direction === 'asc' ? aVal - bVal : bVal - aVal;
}
return 0;
});
</script>
<ModelModal
bind:show={showLeaderboardModal}
model={selectedModel}
{feedbacks}
onClose={closeLeaderboardModal}
/>
<div class="mt-0.5 mb-2 gap-1 flex flex-col md:flex-row justify-between">
<div class="flex md:self-center text-lg font-medium px-0.5 shrink-0 items-center">
<div class=" gap-1">
@@ -324,26 +389,124 @@
class="text-xs text-gray-700 uppercase bg-gray-50 dark:bg-gray-850 dark:text-gray-400 -translate-y-0.5"
>
<tr class="">
<th scope="col" class="px-3 py-1.5 cursor-pointer select-none w-3">
{$i18n.t('RK')}
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none w-3"
on:click={() => setSortKey('rating')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('RK')}
{#if orderBy === 'rating'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-1.5 cursor-pointer select-none">
{$i18n.t('Model')}
<th
scope="col"
class="px-3 py-1.5 cursor-pointer select-none"
on:click={() => setSortKey('name')}
>
<div class="flex gap-1.5 items-center">
{$i18n.t('Model')}
{#if orderBy === 'name'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-fit">
{$i18n.t('Rating')}
<th
scope="col"
class="px-3 py-1.5 text-right cursor-pointer select-none w-fit"
on:click={() => setSortKey('rating')}
>
<div class="flex gap-1.5 items-center justify-end">
{$i18n.t('Rating')}
{#if orderBy === 'rating'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-5">
{$i18n.t('Won')}
<th
scope="col"
class="px-3 py-1.5 text-right cursor-pointer select-none w-5"
on:click={() => setSortKey('won')}
>
<div class="flex gap-1.5 items-center justify-end">
{$i18n.t('Won')}
{#if orderBy === 'won'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
<th scope="col" class="px-3 py-1.5 text-right cursor-pointer select-none w-5">
{$i18n.t('Lost')}
<th
scope="col"
class="px-3 py-1.5 text-right cursor-pointer select-none w-5"
on:click={() => setSortKey('lost')}
>
<div class="flex gap-1.5 items-center justify-end">
{$i18n.t('Lost')}
{#if orderBy === 'lost'}
<span class="font-normal">
{#if direction === 'asc'}
<ChevronUp className="size-2" />
{:else}
<ChevronDown className="size-2" />
{/if}
</span>
{:else}
<span class="invisible">
<ChevronUp className="size-2" />
</span>
{/if}
</div>
</th>
</tr>
</thead>
<tbody class="">
{#each rankedModels as model, modelIdx (model.id)}
<tr class="bg-white dark:bg-gray-900 dark:border-gray-850 text-xs group">
{#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)}
>
<td class="px-3 py-1.5 text-left font-medium text-gray-900 dark:text-white w-fit">
<div class=" line-clamp-1">
{model?.rating !== '-' ? modelIdx + 1 : '-'}
@@ -0,0 +1,77 @@
<script lang="ts">
import Modal from '$lib/components/common/Modal.svelte';
import { getContext } from 'svelte';
export let show = false;
export let model = null;
export let feedbacks = [];
export let onClose: () => void = () => {};
const i18n = getContext('i18n');
const close = () => {
show = false;
onClose();
};
$: topTags = model ? getTopTagsForModel(model.id, feedbacks) : [];
const getTopTagsForModel = (modelId: string, feedbacks: any[], topN = 5) => {
const tagCounts = new Map();
feedbacks
.filter((fb) => fb.data.model_id === modelId)
.forEach((fb) => {
(fb.data.tags || []).forEach((tag) => {
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
});
});
return Array.from(tagCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, topN)
.map(([tag, count]) => ({ tag, count }));
};
</script>
<Modal size="sm" bind:show>
{#if model}
<div class="flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class="text-lg font-medium self-center">
{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>
</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">
{#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>
{/each}
</div>
{:else}
<span>-</span>
{/if}
</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>
</div>
</div>
{/if}
</Modal>
+195 -177
View File
@@ -39,6 +39,7 @@
let STT_OPENAI_API_KEY = '';
let STT_ENGINE = '';
let STT_MODEL = '';
let STT_SUPPORTED_CONTENT_TYPES = '';
let STT_WHISPER_MODEL = '';
let STT_AZURE_API_KEY = '';
let STT_AZURE_REGION = '';
@@ -114,6 +115,7 @@
OPENAI_API_KEY: STT_OPENAI_API_KEY,
ENGINE: STT_ENGINE,
MODEL: STT_MODEL,
SUPPORTED_CONTENT_TYPES: STT_SUPPORTED_CONTENT_TYPES.split(','),
WHISPER_MODEL: STT_WHISPER_MODEL,
DEEPGRAM_API_KEY: STT_DEEPGRAM_API_KEY,
AZURE_API_KEY: STT_AZURE_API_KEY,
@@ -160,6 +162,7 @@
STT_ENGINE = res.stt.ENGINE;
STT_MODEL = res.stt.MODEL;
STT_SUPPORTED_CONTENT_TYPES = (res?.stt?.SUPPORTED_CONTENT_TYPES ?? []).join(',');
STT_WHISPER_MODEL = res.stt.WHISPER_MODEL;
STT_AZURE_API_KEY = res.stt.AZURE_API_KEY;
STT_AZURE_REGION = res.stt.AZURE_REGION;
@@ -184,9 +187,26 @@
<div class=" space-y-3 overflow-y-scroll scrollbar-hidden h-full">
<div class="flex flex-col gap-3">
<div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('STT Settings')}</div>
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Speech-to-Text')}</div>
<div class=" py-0.5 flex w-full justify-between">
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
{#if STT_ENGINE !== 'web'}
<div class="mb-2">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Supported MIME Types')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={STT_SUPPORTED_CONTENT_TYPES}
placeholder={$i18n.t('e.g., audio/wav,audio/mpeg (leave blank for defaults)')}
/>
</div>
</div>
</div>
{/if}
<div class="mb-2 py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Speech-to-Text Engine')}</div>
<div class="flex items-center relative">
<select
@@ -220,7 +240,7 @@
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -246,7 +266,7 @@
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -280,7 +300,7 @@
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Azure Region')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Azure Region')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -293,7 +313,7 @@
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Language Locales')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Language Locales')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -306,7 +326,7 @@
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Endpoint URL')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Endpoint URL')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -319,7 +339,7 @@
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Max Speakers')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Max Speakers')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -333,7 +353,7 @@
</div>
{:else if STT_ENGINE === ''}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('STT Model')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('STT Model')}</div>
<div class="flex w-full">
<div class="flex-1 mr-2">
@@ -416,12 +436,12 @@
{/if}
</div>
<hr class="border-gray-100 dark:border-gray-850" />
<div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('TTS Settings')}</div>
<div class=" mb-2.5 text-base font-medium">{$i18n.t('Text-to-Speech')}</div>
<div class=" py-0.5 flex w-full justify-between">
<hr class=" border-gray-100 dark:border-gray-850 my-2" />
<div class="mb-2 py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Text-to-Speech Engine')}</div>
<div class="flex items-center relative">
<select
@@ -484,7 +504,7 @@
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Azure Region')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Azure Region')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -497,7 +517,7 @@
</div>
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('Endpoint URL')}</div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('Endpoint URL')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
@@ -511,198 +531,196 @@
</div>
{/if}
<hr class="border-gray-100 dark:border-gray-850 my-2" />
{#if TTS_ENGINE === ''}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE}
>
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
{#each voices as voice}
<option
value={voice.voiceURI}
class="bg-gray-100 dark:bg-gray-700"
selected={TTS_VOICE === voice.voiceURI}>{voice.name}</option
>
{/each}
</select>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'transformers'}
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL}
placeholder="CMU ARCTIC speaker embedding name"
/>
<datalist id="model-list">
<option value="tts-1" />
</datalist>
</div>
</div>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(`Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.`)}
To learn more about SpeechT5,
<a
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://github.com/microsoft/SpeechT5"
target="_blank"
>
{$i18n.t(`click here`, {
name: 'SpeechT5'
})}.
</a>
To see the available CMU Arctic speaker embeddings,
<a
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://huggingface.co/datasets/Matthijs/cmu-arctic-xvectors"
target="_blank"
>
{$i18n.t(`click here`)}.
</a>
</div>
</div>
{:else if TTS_ENGINE === 'openai'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
<div class="mb-2">
{#if TTS_ENGINE === ''}
<div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
<select
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<datalist id="voice-list">
>
<option value="" selected={TTS_VOICE !== ''}>{$i18n.t('Default')}</option>
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
<option
value={voice.voiceURI}
class="bg-gray-100 dark:bg-gray-700"
selected={TTS_VOICE === voice.voiceURI}>{voice.name}</option
>
{/each}
</datalist>
</select>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
{:else if TTS_ENGINE === 'transformers'}
<div>
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
list="model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL}
placeholder="Select a model"
placeholder="CMU ARCTIC speaker embedding name"
/>
<datalist id="tts-model-list">
{#each models as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700" />
{/each}
<datalist id="model-list">
<option value="tts-1" />
</datalist>
</div>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'elevenlabs'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t(`Open WebUI uses SpeechT5 and CMU Arctic speaker embeddings.`)}
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL}
placeholder="Select a model"
/>
To learn more about SpeechT5,
<datalist id="tts-model-list">
{#each models as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700" />
{/each}
</datalist>
</div>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'azure'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-sm font-medium">
{$i18n.t('Output format')}
<a
href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs"
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://github.com/microsoft/SpeechT5"
target="_blank"
>
<small>{$i18n.t('Available list')}</small>
{$i18n.t(`click here`, {
name: 'SpeechT5'
})}.
</a>
To see the available CMU Arctic speaker embeddings,
<a
class=" hover:underline dark:text-gray-200 text-gray-800"
href="https://huggingface.co/datasets/Matthijs/cmu-arctic-xvectors"
target="_blank"
>
{$i18n.t(`click here`)}.
</a>
</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
placeholder="Select a output format"
/>
</div>
{:else if TTS_ENGINE === 'openai'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL}
placeholder="Select a model"
/>
<datalist id="tts-model-list">
{#each models as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700" />
{/each}
</datalist>
</div>
</div>
</div>
</div>
</div>
{/if}
{:else if TTS_ENGINE === 'elevenlabs'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<hr class="border-gray-100 dark:border-gray-850 my-2" />
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Model')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_MODEL}
placeholder="Select a model"
/>
<datalist id="tts-model-list">
{#each models as model}
<option value={model.id} class="bg-gray-50 dark:bg-gray-700" />
{/each}
</datalist>
</div>
</div>
</div>
</div>
{:else if TTS_ENGINE === 'azure'}
<div class=" flex gap-2">
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">{$i18n.t('TTS Voice')}</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="voice-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_VOICE}
placeholder="Select a voice"
/>
<datalist id="voice-list">
{#each voices as voice}
<option value={voice.id}>{voice.name}</option>
{/each}
</datalist>
</div>
</div>
</div>
<div class="w-full">
<div class=" mb-1.5 text-xs font-medium">
{$i18n.t('Output format')}
<a
href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/rest-text-to-speech?tabs=streaming#audio-outputs"
target="_blank"
>
<small>{$i18n.t('Available list')}</small>
</a>
</div>
<div class="flex w-full">
<div class="flex-1">
<input
list="tts-model-list"
class="w-full rounded-lg py-2 px-4 text-sm bg-gray-50 dark:text-gray-300 dark:bg-gray-850 outline-hidden"
bind:value={TTS_AZURE_SPEECH_OUTPUT_FORMAT}
placeholder="Select a output format"
/>
</div>
</div>
</div>
</div>
{/if}
</div>
<div class="pt-0.5 flex w-full justify-between">
<div class="self-center text-xs font-medium">{$i18n.t('Response splitting')}</div>
@@ -1144,6 +1144,50 @@
</Tooltip>
</div>
</div>
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Image Compression Width')}</div>
<div class="flex items-center relative">
<Tooltip
content={$i18n.t(
'The width in pixels to compress images to. Leave empty for no compression.'
)}
placement="top-start"
>
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number"
placeholder={$i18n.t('Leave empty for no compression')}
bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_WIDTH}
autocomplete="off"
min="0"
/>
</Tooltip>
</div>
</div>
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Image Compression Height')}
</div>
<div class="flex items-center relative">
<Tooltip
content={$i18n.t(
'The height in pixels to compress images to. Leave empty for no compression.'
)}
placement="top-start"
>
<input
class="flex-1 w-full text-sm bg-transparent outline-hidden"
type="number"
placeholder={$i18n.t('Leave empty for no compression')}
bind:value={RAGConfig.FILE_IMAGE_COMPRESSION_HEIGHT}
autocomplete="off"
min="0"
/>
</Tooltip>
</div>
</div>
</div>
<div class="mb-3">
@@ -65,6 +65,7 @@
},
chat: {
controls: true,
system_prompt: true,
file_upload: true,
delete: true,
edit: true,
@@ -263,6 +263,14 @@
<Switch bind:state={permissions.chat.controls} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Chat System Prompt')}
</div>
<Switch bind:state={permissions.chat.system_prompt} />
</div>
<div class=" flex w-full justify-between my-2 pr-2">
<div class=" self-center text-xs font-medium">
{$i18n.t('Allow Chat Delete')}
@@ -101,7 +101,7 @@
<div class="flex-1">
<select
class="w-full dark:bg-gray-900 rounded-sm text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
class="w-full dark:bg-gray-900 text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
bind:value={_user.role}
disabled={_user.id == sessionUser.id}
required
@@ -118,13 +118,12 @@
<div class="flex-1">
<input
class="w-full rounded-sm text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
class="w-full text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
type="email"
bind:value={_user.email}
placeholder={$i18n.t('Enter Your Email')}
autocomplete="off"
required
disabled={_user.id == sessionUser.id}
/>
</div>
</div>
@@ -134,7 +133,7 @@
<div class="flex-1">
<input
class="w-full rounded-sm text-sm bg-transparent outline-hidden"
class="w-full text-sm bg-transparent outline-hidden"
type="text"
bind:value={_user.name}
placeholder={$i18n.t('Enter Your Name')}
@@ -149,7 +148,7 @@
<div class="flex-1">
<input
class="w-full rounded-sm text-sm bg-transparent outline-hidden"
class="w-full text-sm bg-transparent outline-hidden"
type="password"
placeholder={$i18n.t('Enter New Password')}
bind:value={_user.password}
+24 -3
View File
@@ -110,9 +110,30 @@
reader.onload = async (event) => {
let imageUrl = event.target.result;
if ($settings?.imageCompression ?? false) {
const width = $settings?.imageCompressionSize?.width ?? null;
const height = $settings?.imageCompressionSize?.height ?? null;
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);
@@ -1,10 +1,9 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { createEventDispatcher } from 'svelte';
import { flyAndScale } from '$lib/utils/transitions';
import { WEBUI_BASE_URL } from '$lib/constants';
import { activeUserIds } from '$lib/stores';
import { getUserActiveStatusById } from '$lib/apis/users';
export let side = 'right';
export let align = 'top';
@@ -12,15 +11,29 @@
export let user = null;
let show = false;
const dispatch = createEventDispatcher();
let active = false;
const getActiveStatus = async () => {
const res = await getUserActiveStatusById(localStorage.token, user.id).catch((error) => {
console.error('Error fetching user active status:', error);
});
if (res) {
active = res.active;
} else {
active = false;
}
};
$: if (show) {
getActiveStatus();
}
</script>
<DropdownMenu.Root
bind:open={show}
closeFocus={false}
onOpenChange={(state) => {
dispatch('change', state);
}}
onOpenChange={(state) => {}}
typeahead={false}
>
<DropdownMenu.Trigger>
@@ -52,7 +65,7 @@
</div>
<div class=" flex items-center gap-2">
{#if $activeUserIds.includes(user.id)}
{#if active}
<div>
<span class="relative flex size-2">
<span
+1 -1
View File
@@ -335,7 +335,7 @@
title="Content"
srcdoc={contents[selectedContentIdx].content}
class="w-full border-0 h-full rounded-none"
sandbox="allow-scripts{($settings?.iframeSandboxAllowForms ?? false)
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false)
? ' allow-same-origin'
+36 -23
View File
@@ -432,31 +432,20 @@
}
};
let pageSubscribe = null;
onMount(async () => {
loading = true;
console.log('mounted');
window.addEventListener('message', onMessageHandler);
$socket?.on('chat-events', chatEventHandler);
page.subscribe((page) => {
if (page.url.pathname === '/') {
pageSubscribe = page.subscribe(async (p) => {
if (p.url.pathname === '/') {
await tick();
initNewChat();
}
});
if (!$chatId) {
chatIdUnsubscriber = chatId.subscribe(async (value) => {
if (!value) {
await tick(); // Wait for DOM updates
await initNewChat();
}
});
} else {
if ($temporaryChatEnabled) {
await goto('/');
}
}
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
prompt = '';
files = [];
@@ -515,6 +504,7 @@
});
onDestroy(() => {
pageSubscribe();
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('chat-events', chatEventHandler);
@@ -805,6 +795,11 @@
`https://www.youtube.com/watch?v=${$page.url.searchParams.get('youtube')}`
);
}
if ($page.url.searchParams.get('load-url')) {
await uploadWeb($page.url.searchParams.get('load-url'));
}
if ($page.url.searchParams.get('web-search') === 'true') {
webSearchEnabled = true;
}
@@ -813,6 +808,10 @@
imageGenerationEnabled = true;
}
if ($page.url.searchParams.get('code-interpreter') === 'true') {
codeInterpreterEnabled = true;
}
if ($page.url.searchParams.get('tools')) {
selectedToolIds = $page.url.searchParams
.get('tools')
@@ -859,6 +858,11 @@
const loadChat = async () => {
chatId.set(chatIdProp);
if ($temporaryChatEnabled) {
temporaryChatEnabled.set(false);
}
chat = await getChatById(localStorage.token, $chatId).catch(async (error) => {
await goto('/');
return null;
@@ -878,6 +882,11 @@
(chatContent?.models ?? undefined) !== undefined
? chatContent.models
: [chatContent.models ?? ''];
if (!($user?.role === 'admin' || ($user?.permissions?.chat?.multiple_models ?? true))) {
selectedModels = selectedModels.length > 0 ? [selectedModels[0]] : [''];
}
oldSelectedModelIds = selectedModels;
history =
@@ -1725,6 +1734,7 @@
history.messages[responseMessageId] = responseMessage;
history.currentId = responseMessageId;
return null;
});
@@ -1821,7 +1831,8 @@
childrenIds: [],
role: 'user',
content: userPrompt,
models: selectedModels
models: selectedModels,
timestamp: Math.floor(Date.now() / 1000) // Unix epoch
};
if (parentId !== null) {
@@ -2107,13 +2118,15 @@
{stopResponse}
{createMessagePair}
onChange={(input) => {
if (input.prompt !== null) {
localStorage.setItem(
`chat-input${$chatId ? `-${$chatId}` : ''}`,
JSON.stringify(input)
);
} else {
localStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
if (!$temporaryChatEnabled) {
if (input.prompt !== null) {
localStorage.setItem(
`chat-input${$chatId ? `-${$chatId}` : ''}`,
JSON.stringify(input)
);
} else {
localStorage.removeItem(`chat-input${$chatId ? `-${$chatId}` : ''}`);
}
}
}}
on:upload={async (e) => {
@@ -67,7 +67,7 @@
</div>
</Collapsible>
{#if $user?.role === 'admin' || $user?.permissions.chat?.controls}
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
<Collapsible title={$i18n.t('System Prompt')} open={true} buttonClassName="w-full">
@@ -80,7 +80,9 @@
/>
</div>
</Collapsible>
{/if}
{#if $user?.role === 'admin' || ($user?.permissions.chat?.controls ?? true)}
<hr class="my-2 border-gray-50 dark:border-gray-700/10" />
<Collapsible title={$i18n.t('Advanced Params')} open={true} buttonClassName="w-full">
+46 -4
View File
@@ -91,7 +91,15 @@
$: onChange({
prompt,
files: files.filter((file) => file.type !== 'image'),
files: files
.filter((file) => file.type !== 'image')
.map((file) => {
return {
...file,
user: undefined,
access_control: undefined
};
}),
selectedToolIds,
selectedFilterIds,
imageGenerationEnabled,
@@ -299,6 +307,19 @@
const inputFilesHandler = async (inputFiles) => {
console.log('Input files handler called with:', inputFiles);
if (
($config?.file?.max_count ?? null) !== null &&
files.length + inputFiles.length > $config?.file?.max_count
) {
toast.error(
$i18n.t(`You can only chat with a maximum of {{maxCount}} file(s) at a time.`, {
maxCount: $config?.file?.max_count
})
);
return;
}
inputFiles.forEach((file) => {
console.log('Processing file:', {
name: file.name,
@@ -334,9 +355,30 @@
reader.onload = async (event) => {
let imageUrl = event.target.result;
if ($settings?.imageCompression ?? false) {
const width = $settings?.imageCompressionSize?.width ?? null;
const height = $settings?.imageCompressionSize?.height ?? null;
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);
+4
View File
@@ -256,6 +256,10 @@
};
const editMessage = async (messageId, { content, files }, submit = true) => {
if ((selectedModels ?? []).filter((id) => id).length === 0) {
toast.error($i18n.t('Model not selected'));
return;
}
if (history.messages[messageId].role === 'user') {
if (submit) {
// New user message
@@ -188,9 +188,8 @@
</div>
<div slot="content">
<div class="flex text-xs font-medium flex-wrap">
{#each citations as citation, idx}
{#each citations.slice(2) as citation, idx}
<button
id={`source-${id}-${idx + 1}`}
class="no-toggle outline-hidden flex dark:text-gray-300 p-1 bg-gray-50 hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 transition rounded-xl max-w-96"
on:click={() => {
showCitationModal = true;
@@ -199,7 +198,7 @@
>
{#if citations.every((c) => c.distances !== undefined)}
<div class="bg-gray-50 dark:bg-gray-800 rounded-full size-4">
{idx + 1}
{idx + 3}
</div>
{/if}
<div class="flex-1 mx-1 truncate">
@@ -109,7 +109,7 @@
>
{decodeString(document?.metadata?.name ?? document.source.name)}
</a>
{#if document?.metadata?.page}
{#if Number.isInteger(document?.metadata?.page)}
<span class="text-xs text-gray-500 dark:text-gray-400">
({$i18n.t('page')}
{document.metadata.page + 1})
@@ -69,6 +69,21 @@
>
</iframe>
{/if}
{:else if token.text && token.text.includes('<iframe')}
{@const match = token.text.match(/<iframe\s+[^>]*src="([^"]+)"[^>]*><\/iframe>/)}
{@const iframeSrc = match && match[1]}
{#if iframeSrc}
<iframe
class="w-full my-2"
src={iframeSrc}
title="Embedded content"
frameborder="0"
sandbox
onload="this.style.height=(this.contentWindow.document.body.scrollHeight+20)+'px';"
></iframe>
{:else}
{token.text}
{/if}
{:else if token.text.includes(`<file type="html"`)}
{@const match = token.text.match(/<file type="html" id="([^"]+)"/)}
{@const fileId = match && match[1]}
@@ -78,7 +93,7 @@
src={`${WEBUI_BASE_URL}/api/v1/files/${fileId}/content/html`}
title="Content"
frameborder="0"
sandbox="allow-scripts{($settings?.iframeSandboxAllowForms ?? false)
sandbox="allow-scripts allow-downloads{($settings?.iframeSandboxAllowForms ?? false)
? ' allow-forms'
: ''}{($settings?.iframeSandboxAllowSameOrigin ?? false) ? ' allow-same-origin' : ''}"
referrerpolicy="strict-origin-when-cross-origin"
@@ -225,7 +225,7 @@
<div
class=" snap-center w-full max-w-full m-1 border {history.messages[messageId]
?.modelIdx == modelIdx
? `border-gray-100 dark:border-gray-850 border-[1.5px] ${
? `bg-gray-50 dark:bg-gray-850 border-gray-100 dark:border-gray-800 border-2 ${
$mobile ? 'min-w-full' : 'min-w-80'
}`
: `border-gray-100 dark:border-gray-850 border-dashed ${
@@ -605,7 +605,7 @@
<ProfileImage
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
className={'size-8'}
className={'size-8 assistant-message-profile-image'}
/>
</div>
@@ -15,8 +15,10 @@
<div class="flex flex-col text-left gap-1 mt-1.5">
{#each followUps as followUp, idx (idx)}
<button
class=" mr-2 py-1.5 bg-transparent text-left text-sm flex items-center gap-2 px-1.5 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white transition"
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class=" mr-2 py-1.5 bg-transparent text-left text-sm flex items-center gap-2 px-1.5 text-gray-500 dark:text-gray-400 hover:text-black dark:hover:text-white transition cursor-pointer"
on:click={() => onClick(followUp)}
title={followUp}
aria-label={followUp}
@@ -26,7 +28,7 @@
<div class="line-clamp-1">
{followUp}
</div>
</button>
</div>
{#if idx < followUps.length - 1}
<hr class="border-gray-100 dark:border-gray-850" />
@@ -107,7 +107,11 @@
}}
/>
<div class=" flex w-full user-message" dir={$settings.chatDirection} id="message-{message.id}">
<div
class=" flex w-full user-message group"
dir={$settings.chatDirection}
id="message-{message.id}"
>
{#if !($settings?.chatBubble ?? true)}
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
<ProfileImage
@@ -115,7 +119,7 @@
? ($models.find((m) => m.id === message.user)?.info?.meta?.profile_image_url ??
'/user.png')
: (user?.profile_image_url ?? '/user.png')}
className={'size-8'}
className={'size-8 user-message-profile-image'}
/>
</div>
{/if}
@@ -143,6 +147,16 @@
{/if}
</Name>
</div>
{:else if message.timestamp}
<div class="flex justify-end pb-1 pr-2">
<div
class="text-xs invisible group-hover:visible text-gray-400 font-medium first-letter:capitalize translate-y-[1px]"
>
<Tooltip content={dayjs(message.timestamp * 1000).format('LLLL')}>
<span class="line-clamp-1">{formatDate(message.timestamp * 1000)}</span>
</Tooltip>
</div>
</div>
{/if}
<div class="chat-{message.role} w-full min-w-full markdown-prose">
@@ -425,7 +425,7 @@
class="flex gap-1 w-fit text-center text-sm font-medium rounded-full bg-transparent px-1.5 pb-0.5"
bind:this={tagsContainerElement}
>
{#if (items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')) || items.find((item) => item.model?.direct) || tags.length > 0}
{#if items.find((item) => item.model?.connection_type === 'local') || items.find((item) => item.model?.connection_type === 'external') || items.find((item) => item.model?.direct) || tags.length > 0}
<button
class="min-w-fit outline-none p-1.5 {selectedTag === '' &&
selectedConnectionType === ''
@@ -440,7 +440,7 @@
</button>
{/if}
{#if items.find((item) => item.model?.connection_type === 'local') && items.find((item) => item.model?.connection_type === 'external')}
{#if items.find((item) => item.model?.connection_type === 'local')}
<button
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'local'
? ''
@@ -452,6 +452,9 @@
>
{$i18n.t('Local')}
</button>
{/if}
{#if items.find((item) => item.model?.connection_type === 'external')}
<button
class="min-w-fit outline-none p-1.5 {selectedConnectionType === 'external'
? ''
+33 -17
View File
@@ -47,6 +47,15 @@
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
<button
id="new-chat-button"
class="hidden"
on:click={() => {
initNewChat();
}}
aria-label="New Chat"
/>
<nav class="sticky top-0 z-30 w-full py-1 -mb-8 flex flex-col items-center drag-region">
<div class="flex items-center w-full pl-1.5 pr-1">
<div
@@ -72,6 +81,24 @@
<MenuLines />
</div>
</button>
{#if !$mobile}
<Tooltip content={$i18n.t('New Chat')}>
<button
class=" flex {$showSidebar
? 'md:hidden'
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
initNewChat();
}}
aria-label="New Chat"
>
<div class=" m-auto self-center">
<PencilSquare className=" size-5" strokeWidth="2" />
</div>
</button>
</Tooltip>
{/if}
</div>
<div
@@ -135,23 +162,6 @@
</button>
</Tooltip>
<Tooltip content={$i18n.t('New Chat')}>
<button
id="new-chat-button"
class=" flex {$showSidebar
? 'md:hidden'
: ''} cursor-pointer px-2 py-2 rounded-xl text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-850 transition"
on:click={() => {
initNewChat();
}}
aria-label="New Chat"
>
<div class=" m-auto self-center">
<PencilSquare className=" size-5" strokeWidth="2" />
</div>
</button>
</Tooltip>
{#if $user !== undefined && $user !== null}
<UserMenu
className="max-w-[240px]"
@@ -183,6 +193,12 @@
</div>
</div>
{#if $temporaryChatEnabled && $chatId === 'local'}
<div class=" w-full z-30 text-center">
<div class="text-xs text-gray-500">{$i18n.t('Temporary Chat')}</div>
</div>
{/if}
{#if !history.currentId && !$chatId && ($banners.length > 0 || ($config?.license_metadata?.type ?? null) === 'trial' || (($config?.license_metadata?.seats ?? null) !== null && $config?.user_count > $config?.license_metadata?.seats))}
<div class=" w-full z-30 mt-5">
<div class=" flex flex-col gap-1 w-full">
+21 -8
View File
@@ -93,7 +93,7 @@
<div class="m-auto w-full max-w-6xl px-2 @2xl:px-20 translate-y-6 py-24 text-center">
{#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-center mb-0.5"
placement="top"
>
@@ -107,7 +107,7 @@
class="w-full text-3xl text-gray-800 dark:text-gray-100 text-center flex items-center gap-4 font-primary"
>
<div class="w-full flex flex-col justify-center items-center">
<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5">
<div class="flex flex-row justify-center gap-3 @sm:gap-3.5 w-fit px-5 max-w-xl">
<div class="flex shrink-0 justify-center">
<div class="flex -space-x-4 mb-0.5" in:fade={{ duration: 100 }}>
{#each models as model, modelIdx}
@@ -138,9 +138,20 @@
</div>
</div>
<div class=" text-3xl @sm:text-3xl line-clamp-1" in:fade={{ duration: 100 }}>
<div
class=" text-3xl @sm:text-3xl line-clamp-1 flex items-center"
in:fade={{ duration: 100 }}
>
{#if models[selectedModelIdx]?.name}
{models[selectedModelIdx]?.name}
<Tooltip
content={models[selectedModelIdx]?.name}
placement="top"
className=" flex items-center "
>
<span class="line-clamp-1">
{models[selectedModelIdx]?.name}
</span>
</Tooltip>
{:else}
{$i18n.t('Hello, {{name}}', { name: $user?.name })}
{/if}
@@ -205,10 +216,12 @@
{createMessagePair}
placeholder={$i18n.t('How can I help you today?')}
onChange={(input) => {
if (input.prompt !== null) {
localStorage.setItem(`chat-input`, JSON.stringify(input));
} else {
localStorage.removeItem(`chat-input`);
if (!$temporaryChatEnabled) {
if (input.prompt !== null) {
localStorage.setItem(`chat-input`, JSON.stringify(input));
} else {
localStorage.removeItem(`chat-input`);
}
}
}}
on:upload={(e) => {
@@ -42,7 +42,7 @@
});
</script>
<div class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
<div id="tab-about" class="flex flex-col h-full justify-between space-y-3 text-sm mb-6">
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
<div>
<div class=" mb-2.5 text-sm font-medium flex space-x-2 items-center">
@@ -86,7 +86,7 @@
});
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div id="tab-account" class="flex flex-col h-full justify-between text-sm">
<div class=" overflow-y-scroll max-h-[28rem] lg:max-h-full">
<input
id="profile-image-input"
@@ -91,7 +91,7 @@
<div>
<Tooltip
content={$i18n.t(
'Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model’s built-in tool-calling capabilities, but requires the model to inherently support this feature.'
"Default mode works with a wider range of models by calling tools once before execution. Native mode leverages the model's built-in tool-calling capabilities, but requires the model to inherently support this feature."
)}
placement="top-start"
className="inline-tooltip"
@@ -154,6 +154,7 @@
</script>
<form
id="tab-audio"
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={async () => {
saveSettings({
@@ -107,7 +107,7 @@
<ArchivedChatsModal bind:show={showArchivedChatsModal} onUpdate={handleArchivedChatsChange} />
<div class="flex flex-col h-full justify-between space-y-3 text-sm">
<div id="tab-chats" class="flex flex-col h-full justify-between space-y-3 text-sm">
<div class=" space-y-2 overflow-y-scroll max-h-[28rem] lg:max-h-full">
<div class="flex flex-col">
<input
@@ -70,6 +70,7 @@
<AddConnectionModal direct bind:show={showConnectionModal} onSubmit={addConnectionHandler} />
<form
id="tab-connections"
class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => {
updateHandler();
@@ -126,7 +127,11 @@
</div>
<div class="my-1.5">
<div class="text-xs text-gray-500">
<div
class="text-xs {($settings?.highContrastMode ?? false)
? 'text-gray-800 dark:text-gray-100'
: 'text-gray-500'}"
>
{$i18n.t('Connect to your own OpenAI compatible API endpoints.')}
<br />
{$i18n.t(
@@ -2,6 +2,7 @@
import { getContext, tick } from 'svelte';
const i18n = getContext('i18n');
import { settings } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import SensitiveInput from '$lib/components/common/SensitiveInput.svelte';
import Cog6 from '$lib/components/icons/Cog6.svelte';
@@ -65,7 +66,7 @@
<div class="flex w-full gap-2">
<div class="flex-1 relative">
<input
class=" outline-hidden w-full bg-transparent {pipeline ? 'pr-8' : ''}"
class={`w-full bg-transparent ${($settings?.highContrastMode ?? false) ? '' : 'outline-hidden'} ${pipeline ? 'pr-8' : ''}`}
placeholder={$i18n.t('API Base URL')}
bind:value={url}
autocomplete="off"
@@ -73,7 +74,7 @@
</div>
<SensitiveInput
inputClassName=" outline-hidden bg-transparent w-full"
inputClassName="bg-transparent w-full"
placeholder={$i18n.t('API Key')}
bind:value={key}
/>
@@ -83,6 +84,7 @@
<div class="flex gap-1">
<Tooltip content={$i18n.t('Configure')} className="self-start">
<button
aria-label={$i18n.t('Open modal to configure connection')}
class="self-center p-1 bg-transparent hover:bg-gray-100 dark:bg-gray-900 dark:hover:bg-gray-850 rounded-lg transition"
on:click={() => {
showConfigModal = true;
@@ -187,7 +187,7 @@
};
</script>
<div class="flex flex-col h-full justify-between text-sm">
<div class="flex flex-col h-full justify-between text-sm" id="tab-general">
<div class=" overflow-y-scroll max-h-[28rem] lg:max-h-full">
<div class="">
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Settings')}</div>
@@ -196,7 +196,9 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Theme')}</div>
<div class="flex items-center relative">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent outline-hidden text-right"
class="dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent text-right {$settings.highContrastMode
? ''
: 'outline-hidden'}"
bind:value={selectedTheme}
placeholder="Select a theme"
on:change={() => themeChangeHandler(selectedTheme)}
@@ -216,7 +218,9 @@
<div class=" self-center text-xs font-medium">{$i18n.t('Language')}</div>
<div class="flex items-center relative">
<select
class=" dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent outline-hidden text-right"
class="dark:bg-gray-900 w-fit pr-8 rounded-sm py-2 px-2 text-xs bg-transparent text-right {$settings.highContrastMode
? ''
: 'outline-hidden'}"
bind:value={lang}
placeholder="Select a language"
on:change={(e) => {
@@ -263,7 +267,7 @@
</div>
</div>
{#if $user?.role === 'admin' || $user?.permissions.chat?.controls}
{#if $user?.role === 'admin' || ($user?.permissions.chat?.system_prompt ?? true)}
<hr class="border-gray-50 dark:border-gray-850 my-3" />
<div>
@@ -275,7 +279,9 @@
placeholder={$i18n.t('Enter system prompt here')}
/>
</div>
{/if}
{#if $user?.role === 'admin' || ($user?.permissions.chat?.controls ?? true)}
<div class="mt-2 space-y-3 pr-1.5">
<div class="flex justify-between items-center text-sm">
<div class=" font-medium">{$i18n.t('Advanced Parameters')}</div>
+104 -35
View File
@@ -348,6 +348,7 @@
</script>
<form
id="tab-interface"
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
updateInterfaceHandler();
@@ -384,15 +385,16 @@
<div class=" space-y-3 overflow-y-scroll max-h-[28rem] lg:max-h-full">
<div>
<div class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</div>
<h1 class=" mb-1.5 text-sm font-medium">{$i18n.t('UI')}</h1>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="high-contrast-mode-label" class=" self-center text-xs">
{$i18n.t('High Contrast Mode')} ({$i18n.t('Beta')})
</div>
<button
aria-labelledby="high-contrast-mode-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleHighContrastMode();
@@ -410,9 +412,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Landing Page Mode')}</div>
<div id="landing-page-mode-label" class=" self-center text-xs">
{$i18n.t('Landing Page Mode')}
</div>
<button
aria-labelledby="landing-page-mode-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleLandingPageMode();
@@ -430,9 +435,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Chat Bubble UI')}</div>
<div id="chat-bubble-ui-label" class=" self-center text-xs">
{$i18n.t('Chat Bubble UI')}
</div>
<button
aria-labelledby="chat-bubble-ui-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleChatBubble();
@@ -451,11 +459,12 @@
{#if !$settings.chatBubble}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="chat-bubble-username-label" class=" self-center text-xs">
{$i18n.t('Display the username instead of You in the Chat')}
</div>
<button
aria-labelledby="chat-bubble-username-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleShowUsername();
@@ -474,13 +483,16 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Widescreen Mode')}</div>
<div id="widescreen-mode-label" class=" self-center text-xs">
{$i18n.t('Widescreen Mode')}
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleWidescreenMode();
}}
aria-labelledby="widescreen-mode-label"
type="button"
>
{#if widescreenMode === true}
@@ -494,9 +506,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Chat direction')}</div>
<div id="chat-direction-label" class=" self-center text-xs">
{$i18n.t('Chat direction')}
</div>
<button
aria-labelledby="chat-direction-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={toggleChangeChatDirection}
type="button"
@@ -513,12 +528,13 @@
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div class="py-0.5 flex w-full justify-between">
<div id="notification-sound-label" class=" self-center text-xs">
{$i18n.t('Notification Sound')}
</div>
<button
aria-labelledby="notification-sound-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleNotificationSound();
@@ -537,11 +553,12 @@
{#if notificationSound}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="play-notification-sound-label" class=" self-center text-xs">
{$i18n.t('Always Play Notification Sound')}
</div>
<button
aria-labelledby="play-notification-sound-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleNotificationSoundAlways();
@@ -561,11 +578,12 @@
{#if $user?.role === 'admin'}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="toast-notifications-label" class=" self-center text-xs">
{$i18n.t('Toast notifications for new updates')}
</div>
<button
aria-labelledby="toast-notifications-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleShowUpdateToast();
@@ -583,11 +601,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="whats-new-label" class=" self-center text-xs">
{$i18n.t(`Show "What's New" modal on login`)}
</div>
<button
aria-labelledby="whats-new-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleShowChangelog();
@@ -608,9 +627,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Title Auto-Generation')}</div>
<div id="auto-generation-label" class=" self-center text-xs">
{$i18n.t('Title Auto-Generation')}
</div>
<button
aria-labelledby="auto-generation-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleTitleAutoGenerate();
@@ -631,6 +653,7 @@
<div class=" self-center text-xs">{$i18n.t('Follow-Up Auto-Generation')}</div>
<button
aria-labelledby="auto-generation-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleAutoFollowUps();
@@ -648,9 +671,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Chat Tags Auto-Generation')}</div>
<div id="chat-tags-label" class=" self-center text-xs">
{$i18n.t('Chat Tags Auto-Generation')}
</div>
<button
aria-labelledby="chat-tags-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleAutoTags();
@@ -668,11 +694,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="detect-artifacts-label" class=" self-center text-xs">
{$i18n.t('Detect Artifacts Automatically')}
</div>
<button
aria-labelledby="detect-artifacts-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleDetectArtifacts();
@@ -690,11 +717,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="auto-copy-label" class=" self-center text-xs">
{$i18n.t('Auto-Copy Response to Clipboard')}
</div>
<button
aria-labelledby="auto-copy-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleResponseAutoCopy();
@@ -712,11 +740,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="rich-input-label" class=" self-center text-xs">
{$i18n.t('Rich Text Input for Chat')}
</div>
<button
aria-labelledby="rich-input-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleRichTextInput();
@@ -735,11 +764,12 @@
{#if $config?.features?.enable_autocomplete_generation && richTextInput}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="prompt-autocompletion-label" class=" self-center text-xs">
{$i18n.t('Prompt Autocompletion')}
</div>
<button
aria-labelledby="prompt-autocompletion-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
togglePromptAutocomplete();
@@ -758,11 +788,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="paste-large-label" class=" self-center text-xs">
{$i18n.t('Paste Large Text as File')}
</div>
<button
aria-labelledby="paste-large-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleLargeTextAsFile();
@@ -780,11 +811,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="copy-formatted-label" class=" self-center text-xs">
{$i18n.t('Copy Formatted Text')}
</div>
<button
aria-labelledby="copy-formatted-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleCopyFormatted();
@@ -802,9 +834,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Always Collapse Code Blocks')}</div>
<div id="always-collapse-label" class=" self-center text-xs">
{$i18n.t('Always Collapse Code Blocks')}
</div>
<button
aria-labelledby="always-collapse-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleCollapseCodeBlocks();
@@ -822,9 +857,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Always Expand Details')}</div>
<div id="always-expand-label" class=" self-center text-xs">
{$i18n.t('Always Expand Details')}
</div>
<button
aria-labelledby="always-expand-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleExpandDetails();
@@ -842,11 +880,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="chat-background-label" class=" self-center text-xs">
{$i18n.t('Chat Background Image')}
</div>
<button
aria-labelledby="chat-background-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
if (backgroundImageUrl !== null) {
@@ -868,10 +907,11 @@
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div id="allow-user-location-label" class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Allow User Location')}</div>
<button
aria-labelledby="allow-user-location-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleUserLocation();
@@ -889,11 +929,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="haptic-feedback-label" class=" self-center text-xs">
{$i18n.t('Haptic Feedback')} ({$i18n.t('Android')})
</div>
<button
aria-labelledby="haptic-feedback-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleHapticFeedback();
@@ -911,11 +952,12 @@
<!-- <div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="fluidly-stream-label" class=" self-center text-xs">
{$i18n.t('Fluidly stream large external response chunks')}
</div>
<button
aria-labelledby="fluidly-stream-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleSplitLargeChunks();
@@ -933,11 +975,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="enter-key-behavior-label" class=" self-center text-xs">
{$i18n.t('Enter Key Behavior')}
</div>
<button
aria-labelledby="enter-key-behavior-label"
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
togglectrlEnterToSend();
@@ -955,11 +998,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="scroll-on-branch-change-label" class=" self-center text-xs">
{$i18n.t('Scroll On Branch Change')}
</div>
<button
aria-labelledby="scroll-on-branch-change-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
togglesScrollOnBranchChange();
@@ -977,9 +1021,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Web Search in Chat')}</div>
<div id="web-search-in-chat-label" class=" self-center text-xs">
{$i18n.t('Web Search in Chat')}
</div>
<button
aria-labelledby="web-search-in-chat-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleWebSearch();
@@ -997,9 +1044,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('iframe Sandbox Allow Same Origin')}</div>
<div id="iframe-sandbox-allow-same-origin-label" class=" self-center text-xs">
{$i18n.t('iframe Sandbox Allow Same Origin')}
</div>
<button
aria-labelledby="iframe-sandbox-allow-same-origin-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleIframeSandboxAllowSameOrigin();
@@ -1017,9 +1067,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('iframe Sandbox Allow Forms')}</div>
<div id="iframe-sandbox-allow-forms-label" class=" self-center text-xs">
{$i18n.t('iframe Sandbox Allow Forms')}
</div>
<button
aria-labelledby="iframe-sandbox-allow-forms-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleIframeSandboxAllowForms();
@@ -1037,11 +1090,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">
<div id="stylized-pdf-export-label" class=" self-center text-xs">
{$i18n.t('Stylized PDF Export')}
</div>
<button
aria-labelledby="stylized-pdf-export-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleStylizedPdfExport();
@@ -1064,6 +1118,7 @@
<div class=" self-center text-xs">{$i18n.t('Allow Voice Interruption in Call')}</div>
<button
aria-labelledby="allow-voice-interruption-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleVoiceInterruption();
@@ -1081,9 +1136,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Display Emoji in Call')}</div>
<div id="display-emoji-label" class=" self-center text-xs">
{$i18n.t('Display Emoji in Call')}
</div>
<button
aria-labelledby="display-emoji-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleEmojiInCall();
@@ -1103,9 +1161,12 @@
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs">{$i18n.t('Image Compression')}</div>
<div id="image-compression-label" class=" self-center text-xs">
{$i18n.t('Image Compression')}
</div>
<button
aria-labelledby="image-compression-label"
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleImageCompression();
@@ -1124,9 +1185,14 @@
{#if imageCompression}
<div>
<div class=" py-0.5 flex w-full justify-between text-xs">
<div class=" self-center text-xs">{$i18n.t('Image Max Compression Size')}</div>
<div id="image-compression-size-label" class=" self-center text-xs">
{$i18n.t('Image Max Compression Size')}
</div>
<div>
<label class="sr-only" for="image-comp-width"
>{$i18n.t('Image Max Compression Size width')}</label
>
<input
bind:value={imageCompressionSize.width}
type="number"
@@ -1134,6 +1200,9 @@
min="0"
placeholder="Width"
/>x
<label class="sr-only" for="image-comp-height"
>{$i18n.t('Image Max Compression Size height')}</label
>
<input
bind:value={imageCompressionSize.height}
type="number"
@@ -24,6 +24,7 @@
<ManageModal bind:show={showManageModal} />
<form
id="tab-personalization"
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
dispatch('save');
@@ -42,6 +42,7 @@
<AddServerModal bind:show={showConnectionModal} onSubmit={addConnectionHandler} direct />
<form
id="tab-tools"
class="flex flex-col h-full justify-between text-sm"
on:submit|preventDefault={() => {
updateHandler();
@@ -60,6 +61,7 @@
<Tooltip content={$i18n.t(`Add Connection`)}>
<button
aria-label={$i18n.t(`Add Connection`)}
class="px-1"
on:click={() => {
showConnectionModal = true;
@@ -89,7 +91,10 @@
</div>
<div class="my-1.5">
<div class="text-xs text-gray-500">
<div
class={`text-xs
${($settings?.highContrastMode ?? false) ? 'text-gray-800 dark:text-gray-100' : 'text-gray-500'}`}
>
{$i18n.t('Connect to your own OpenAPI compatible external tool servers.')}
<br />
{$i18n.t(
+41 -5
View File
@@ -211,7 +211,9 @@
: []),
...($user?.role === 'admin' ||
($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)
($user?.role === 'user' &&
$user?.permissions?.features?.direct_tool_servers &&
$config?.features?.direct_tool_servers)
? [
{
id: 'tools',
@@ -554,6 +556,7 @@
<div class="flex flex-col md:flex-row w-full px-4 pt-1 pb-4 md:space-x-4">
<div
role="tablist"
id="settings-tabs-container"
class="tabs flex flex-row overflow-x-auto gap-2.5 md:gap-1 md:flex-col flex-1 md:flex-none md:w-40 md:min-h-[32rem] md:max-h-[32rem] dark:text-gray-200 text-sm font-medium text-left mb-1 md:mb-0 -translate-y-1"
>
@@ -574,11 +577,13 @@
placeholder={$i18n.t('Search')}
/>
</div>
{#if visibleTabs.length > 0}
{#each visibleTabs as tabId (tabId)}
{#if tabId === 'general'}
<button
role="tab"
aria-controls="tab-general"
aria-selected={selectedTab === 'general'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'general'
@@ -596,6 +601,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
@@ -611,6 +617,9 @@
</button>
{:else if tabId === 'interface'}
<button
role="tab"
aria-controls="tab-interface"
aria-selected={selectedTab === 'interface'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'interface'
@@ -628,6 +637,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
@@ -644,6 +654,9 @@
{:else if tabId === 'connections'}
{#if $user?.role === 'admin' || ($user?.role === 'user' && $config?.features?.enable_direct_connections)}
<button
role="tab"
aria-controls="tab-connections"
aria-selected={selectedTab === 'connections'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'connections'
@@ -661,6 +674,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
@@ -674,8 +688,11 @@
</button>
{/if}
{:else if tabId === 'tools'}
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers)}
{#if $user?.role === 'admin' || ($user?.role === 'user' && $user?.permissions?.features?.direct_tool_servers && $config?.features?.direct_tool_servers)}
<button
role="tab"
aria-controls="tab-tools"
aria-selected={selectedTab === 'tools'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'tools'
@@ -693,6 +710,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
@@ -709,6 +727,9 @@
{/if}
{:else if tabId === 'personalization'}
<button
role="tab"
aria-controls="tab-personalization"
aria-selected={selectedTab === 'personalization'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'personalization'
@@ -730,6 +751,9 @@
</button>
{:else if tabId === 'audio'}
<button
role="tab"
aria-controls="tab-audio"
aria-selected={selectedTab === 'audio'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'audio'
@@ -747,6 +771,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
@@ -763,6 +788,9 @@
</button>
{:else if tabId === 'chats'}
<button
role="tab"
aria-controls="tab-chats"
aria-selected={selectedTab === 'chats'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'chats'
@@ -780,6 +808,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
@@ -795,6 +824,9 @@
</button>
{:else if tabId === 'account'}
<button
role="tab"
aria-controls="tab-account"
aria-selected={selectedTab === 'account'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'account'
@@ -812,6 +844,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
@@ -827,6 +860,9 @@
</button>
{:else if tabId === 'about'}
<button
role="tab"
aria-controls="tab-about"
aria-selected={selectedTab === 'about'}
class={`px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition
${
selectedTab === 'about'
@@ -844,6 +880,7 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
@@ -864,7 +901,6 @@
{$i18n.t('No results found')}
</div>
{/if}
{#if $user?.role === 'admin'}
<a
href="/admin/settings"
@@ -880,9 +916,9 @@
<div class=" self-center mr-2">
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
class="size-4"
>
<path
+60 -2
View File
@@ -2,6 +2,7 @@
import { getContext } from 'svelte';
import Modal from '../common/Modal.svelte';
import Tooltip from '../common/Tooltip.svelte';
const i18n = getContext('i18n');
export let show = false;
@@ -75,6 +76,26 @@
</div>
</div>
<div class="w-full flex justify-between items-center">
<div class=" text-sm">
<Tooltip
content={$i18n.t(
'Only active when the chat input is in focus and an LLM is generating a response.'
)}
>
{$i18n.t('Stop Generating')}<span class="text-xs"> *</span>
</Tooltip>
</div>
<div class="flex space-x-1 text-xs">
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Esc
</div>
</div>
</div>
<div class="w-full flex justify-between items-center">
<div class=" text-sm">{$i18n.t('Copy last code block')}</div>
@@ -123,6 +144,40 @@
</div>
</div>
<div class="w-full flex justify-between items-center">
<div class=" text-sm">
<Tooltip
content={$i18n.t(
'Only active when "Paste Large Text as File" setting is toggled on.'
)}
>
{$i18n.t('Prevent file creation')}<span class="text-s"> *</span>
</Tooltip>
</div>
<div class="flex space-x-1 text-xs">
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Ctrl/⌘
</div>
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
Shift
</div>
<div
class=" h-fit py-1 px-2 flex items-center justify-center rounded-sm border border-black/10 capitalize text-gray-600 dark:border-white/10 dark:text-gray-300"
>
V
</div>
</div>
</div>
</div>
<div class="flex flex-col space-y-3 w-full self-start">
<div class="w-full flex justify-between items-center">
<div class=" text-sm">{$i18n.t('Generate prompt pair')}</div>
@@ -146,9 +201,7 @@
</div>
</div>
</div>
</div>
<div class="flex flex-col space-y-3 w-full self-start">
<div class="w-full flex justify-between items-center">
<div class=" text-sm">{$i18n.t('Toggle search')}</div>
@@ -251,6 +304,11 @@
</div>
</div>
<div class="px-5 pb-4 text-xs text-gray-500 dark:text-gray-400">
{$i18n.t(
'Shortcuts with an asterisk (*) are situational and only active under specific conditions.'
)}
</div>
<div class=" flex justify-between dark:text-gray-300 px-5">
<div class=" text-lg font-medium self-center">{$i18n.t('Input commands')}</div>
</div>
+1 -1
View File
@@ -83,7 +83,7 @@
{/if}
</div>
<div class="flex-1 text-xs text-gray-700 dark:text-white">
<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>
</div>
@@ -1,4 +1,7 @@
<script lang="ts">
const i18n = getContext('i18n');
import { getContext } from 'svelte';
import { settings } from '$lib/stores';
export let value: string = '';
export let placeholder = '';
export let required = true;
@@ -12,9 +15,11 @@
</script>
<div class={outerClassName}>
<label class="sr-only" for="password-input">{placeholder || $i18n.t('Password')}</label>
<input
class={`${inputClassName} ${show ? '' : 'password'}`}
class={`${inputClassName} ${show ? '' : 'password'} ${($settings?.highContrastMode ?? false) ? '' : ' outline-hidden'}`}
{placeholder}
id="password-input"
bind:value
required={required && !readOnly}
disabled={readOnly}
@@ -24,6 +29,8 @@
<button
class={showButtonClassName}
type="button"
aria-pressed={show}
aria-label={$i18n.t('Make password visible in the user interface')}
on:click={(e) => {
e.preventDefault();
show = !show;
@@ -34,6 +41,7 @@
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
aria-hidden="true"
class="size-4"
>
<path
@@ -51,6 +59,7 @@
viewBox="0 0 16 16"
fill="currentColor"
class="size-4"
aria-hidden="true"
>
<path d="M8 9.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3Z" />
<path
+2
View File
@@ -2,6 +2,7 @@
import { createEventDispatcher, tick } from 'svelte';
import { Switch } from 'bits-ui';
export let state = true;
export let id = '';
const dispatch = createEventDispatcher();
@@ -10,6 +11,7 @@
<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
? ' bg-emerald-600'
: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
+2 -2
View File
@@ -9,7 +9,7 @@
export let tags = [];
</script>
<div class="flex flex-row flex-wrap gap-1 line-clamp-1">
<ul class="flex flex-row flex-wrap gap-1 line-clamp-1">
<TagList
{tags}
on:delete={(e) => {
@@ -23,4 +23,4 @@
dispatch('add', e.detail);
}}
/>
</div>
</ul>
@@ -29,6 +29,7 @@
bind:value={tagName}
class=" px-2 cursor-pointer self-center text-xs h-fit bg-transparent outline-hidden line-clamp-1 w-[6.5rem]"
placeholder={$i18n.t('Add a tag')}
aria-label={$i18n.t('Add a tag')}
list="tagOptions"
on:keydown={(event) => {
if (event.key === 'Enter') {
@@ -48,6 +49,7 @@
viewBox="0 0 16 16"
fill="currentColor"
stroke-width="2"
aria-hidden="true"
class="w-3 h-3"
>
<path
@@ -72,6 +74,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
aria-hidden="true"
fill="currentColor"
class="w-3 h-3 {showTagInput ? 'rotate-45' : ''} transition-all transform"
>
@@ -1,5 +1,7 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { getContext } from 'svelte';
const i18n = getContext('i18n');
import Tooltip from '../Tooltip.svelte';
import XMark from '$lib/components/icons/XMark.svelte';
import Badge from '../Badge.svelte';
@@ -10,7 +12,7 @@
{#each tags as tag}
<Tooltip content={tag.name}>
<div
<li
class="relative group/tags px-1.5 py-[0.2px] gap-0.5 flex justify-between h-fit max-h-fit w-fit items-center rounded-full bg-gray-500/20 text-gray-700 dark:text-gray-200 transition cursor-pointer"
>
<div class=" text-[0.7rem] font-medium self-center line-clamp-1 w-fit">
@@ -23,10 +25,11 @@
dispatch('delete', tag.name);
}}
type="button"
aria-label={$i18n.t('Remove this tag from list')}
>
<XMark className="size-3" strokeWidth="2.5" />
</button>
</div>
</div>
</li>
</Tooltip>
{/each}
+19
View File
@@ -0,0 +1,19 @@
<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"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.25 9.75 16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z"
/>
</svg>
+1
View File
@@ -8,6 +8,7 @@
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
aria-hidden="true"
stroke="currentColor"
class={className}
>
+1
View File
@@ -6,6 +6,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
aria-hidden="true"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
+1
View File
@@ -10,6 +10,7 @@
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
aria-hidden="true"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
+20
View File
@@ -0,0 +1,20 @@
<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"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
+16
View File
@@ -0,0 +1,16 @@
<script lang="ts">
export let className: string = 'w-5 h-5';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class={className}>
<path
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"
/>
<path
fill-rule="evenodd"
d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
clip-rule="evenodd"
/>
</svg>
+19
View File
@@ -0,0 +1,19 @@
<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"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
+9 -9
View File
@@ -250,7 +250,7 @@
{#if $mobile}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
id="chat-controls-button"
on:click={async () => {
await showControls.set(true);
@@ -265,7 +265,7 @@
{#if !$temporaryChatEnabled && ($user?.role === 'admin' || ($user.permissions?.chat?.share ?? true))}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
id="chat-share-button"
on:click={() => {
shareHandler();
@@ -288,7 +288,7 @@
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
id="chat-overview-button"
on:click={async () => {
await showControls.set(true);
@@ -301,7 +301,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
id="chat-overview-button"
on:click={async () => {
await showControls.set(true);
@@ -315,7 +315,7 @@
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -341,7 +341,7 @@
>
{#if $user?.role === 'admin' || ($user.permissions?.chat?.export ?? true)}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
on:click={() => {
downloadJSONExport();
}}
@@ -350,7 +350,7 @@
</DropdownMenu.Item>
{/if}
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
on:click={() => {
downloadTxt();
}}
@@ -359,7 +359,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
on:click={() => {
downloadPdf();
}}
@@ -370,7 +370,7 @@
</DropdownMenu.Sub>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md select-none w-full"
id="chat-copy-button"
on:click={async () => {
const res = await copyToClipboard(await getChatAsText()).catch((e) => {
+4 -13
View File
@@ -490,10 +490,9 @@
draggable="false"
on:click={async () => {
selectedChatId = null;
await goto('/');
const newChatButton = document.getElementById('new-chat-button');
await temporaryChatEnabled.set(false);
setTimeout(() => {
newChatButton?.click();
if ($mobile) {
showSidebar.set(false);
}
@@ -505,7 +504,7 @@
<img
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
class=" size-5 -translate-x-1.5 rounded-full"
class="sidebar-new-chat-icon size-5 -translate-x-1.5 rounded-full"
alt="logo"
/>
</div>
@@ -645,11 +644,7 @@
</div>
{/if}
<div
class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden {$temporaryChatEnabled
? 'opacity-20'
: ''}"
>
<div class="relative flex flex-col flex-1 overflow-y-auto overflow-x-hidden">
{#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
<div class="mt-0.5">
{#each $settings.pinnedModels as modelId (modelId)}
@@ -773,10 +768,6 @@
}
}}
>
{#if $temporaryChatEnabled}
<div class="absolute z-40 w-full h-full flex justify-center"></div>
{/if}
{#if $pinnedChats.length > 0}
<div class="flex flex-col space-y-1 rounded-xl">
<Folder
+76 -101
View File
@@ -4,15 +4,23 @@
import { flyAndScale } from '$lib/utils/transitions';
import { goto } from '$app/navigation';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import { showSettings, activeUserIds, USAGE_POOL, mobile, showSidebar, user } from '$lib/stores';
import { fade, slide } from 'svelte/transition';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { getUsage } from '$lib/apis';
import { userSignOut } from '$lib/apis/auths';
import { showSettings, mobile, showSidebar, user } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import QuestionMarkCircle from '$lib/components/icons/QuestionMarkCircle.svelte';
import Map from '$lib/components/icons/Map.svelte';
import Keyboard from '$lib/components/icons/Keyboard.svelte';
import ShortcutsModal from '$lib/components/chat/ShortcutsModal.svelte';
import Settings from '$lib/components/icons/Settings.svelte';
import Code from '$lib/components/icons/Code.svelte';
import UserGroup from '$lib/components/icons/UserGroup.svelte';
import SignOut from '$lib/components/icons/SignOut.svelte';
const i18n = getContext('i18n');
@@ -24,10 +32,28 @@
let showShortcuts = false;
const dispatch = createEventDispatcher();
let usage = null;
const getUsageInfo = async () => {
const res = await getUsage(localStorage.token).catch((error) => {
console.error('Error fetching usage info:', error);
});
if (res) {
usage = res;
} else {
usage = null;
}
};
$: if (show) {
getUsageInfo();
}
</script>
<ShortcutsModal bind:show={showShortcuts} />
<!-- svelte-ignore a11y-no-static-element-interactions -->
<DropdownMenu.Root
bind:open={show}
onOpenChange={(state) => {
@@ -58,25 +84,7 @@
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<Settings className="w-5 h-5" strokeWidth="1.5" />
</div>
<div class=" self-center truncate">{$i18n.t('Settings')}</div>
</button>
@@ -99,10 +107,10 @@
</button>
{#if role === 'admin'}
<a
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
href="/playground"
on:click={() => {
goto('/playground');
show = false;
if ($mobile) {
@@ -111,28 +119,15 @@
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M14.25 9.75 16.5 12l-2.25 2.25m-4.5 0L7.5 12l2.25-2.25M6 20.25h12A2.25 2.25 0 0 0 20.25 18V6A2.25 2.25 0 0 0 18 3.75H6A2.25 2.25 0 0 0 3.75 6v12A2.25 2.25 0 0 0 6 20.25Z"
/>
</svg>
<Code className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center truncate">{$i18n.t('Playground')}</div>
</a>
</button>
<a
<button
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
href="/admin"
on:click={() => {
goto('/admin');
show = false;
if ($mobile) {
@@ -141,23 +136,10 @@
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
<UserGroup className="w-5 h-5" strokeWidth="1.5" />
</div>
<div class=" self-center truncate">{$i18n.t('Admin Panel')}</div>
</a>
</button>
{/if}
{#if help}
@@ -165,10 +147,11 @@
<!-- {$i18n.t('Help')} -->
<DropdownMenu.Item
class="flex gap-2 items-center py-1.5 px-3 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition"
id="chat-share-button"
on:click={() => {
window.open('https://docs.openwebui.com', '_blank');
show = false;
}}
>
<QuestionMarkCircle className="size-5" />
@@ -177,10 +160,11 @@
<!-- Releases -->
<DropdownMenu.Item
class="flex gap-2 items-center py-1.5 px-3 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition"
id="menu-item-releases"
on:click={() => {
window.open('https://github.com/open-webui/open-webui/releases', '_blank');
show = false;
}}
>
<Map className="size-5" />
@@ -188,7 +172,7 @@
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center py-1.5 px-3 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center py-1.5 px-3 text-sm select-none w-full cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md transition"
id="chat-share-button"
on:click={() => {
showShortcuts = !showShortcuts;
@@ -214,55 +198,46 @@
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
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"
/>
<path
fill-rule="evenodd"
d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
clip-rule="evenodd"
/>
</svg>
<SignOut className="w-5 h-5" strokeWidth="1.5" />
</div>
<div class=" self-center truncate">{$i18n.t('Sign Out')}</div>
</button>
{#if $activeUserIds?.length > 0}
<hr class=" border-gray-100 dark:border-gray-800 my-1 p-0" />
{#if usage}
{#if usage?.user_ids?.length > 0}
<hr class=" border-gray-100 dark:border-gray-800 my-1 p-0" />
<Tooltip
content={$USAGE_POOL && $USAGE_POOL.length > 0
? `${$i18n.t('Running')}: ${$USAGE_POOL.join(', ')} ✨`
: ''}
>
<div class="flex rounded-md py-1 px-3 text-xs gap-2.5 items-center">
<div class=" flex items-center">
<span class="relative flex size-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
/>
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
</span>
</div>
<Tooltip
content={usage?.model_ids && usage?.model_ids.length > 0
? `${$i18n.t('Running')}: ${usage.model_ids.join(', ')} ✨`
: ''}
>
<div
class="flex rounded-md py-1 px-3 text-xs gap-2.5 items-center"
on:mouseenter={() => {
getUsageInfo();
}}
>
<div class=" flex items-center">
<span class="relative flex size-2">
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"
/>
<span class="relative inline-flex rounded-full size-2 bg-green-500" />
</span>
</div>
<div class=" ">
<span class="">
{$i18n.t('Active Users')}:
</span>
<span class=" font-semibold">
{$activeUserIds?.length}
</span>
<div class=" ">
<span class="">
{$i18n.t('Active Users')}:
</span>
<span class=" font-semibold">
{usage?.user_ids?.length}
</span>
</div>
</div>
</div>
</Tooltip>
</Tooltip>
{/if}
{/if}
<!-- <DropdownMenu.Item class="flex items-center py-1.5 px-3 text-sm ">
@@ -5,7 +5,7 @@
import { flyAndScale } from '$lib/utils/transitions';
import { fade, slide } from 'svelte/transition';
import { showSettings, activeUserIds, USAGE_POOL, mobile, showSidebar, user } from '$lib/stores';
import { showSettings, mobile, showSidebar, user } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
+1 -1
View File
@@ -2,7 +2,7 @@
import { DropdownMenu } from 'bits-ui';
import { createEventDispatcher, getContext, onMount } from 'svelte';
import { showSettings, activeUserIds, USAGE_POOL, mobile, showSidebar, user } from '$lib/stores';
import { showSettings, mobile, showSidebar, user } from '$lib/stores';
import { fade, slide } from 'svelte/transition';
import Mic from '../icons/Mic.svelte';