Merge branch 'dev' into leaderboard-evaluation-additions

This commit is contained in:
Ayana H
2025-06-12 22:41:20 -07:00
committed by GitHub
166 changed files with 4524 additions and 1711 deletions
+4 -2
View File
@@ -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
+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>
+28 -4
View File
@@ -1,6 +1,8 @@
<script>
import { getContext, tick, onMount } from 'svelte';
import { toast } from 'svelte-sonner';
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import Leaderboard from './Evaluations/Leaderboard.svelte';
import Feedbacks from './Evaluations/Feedbacks.svelte';
@@ -8,7 +10,24 @@
const i18n = getContext('i18n');
let selectedTab = 'leaderboard';
let selectedTab;
$: {
const pathParts = $page.url.pathname.split('/');
const tabFromPath = pathParts[pathParts.length - 1];
selectedTab = ['leaderboard', 'feedbacks'].includes(tabFromPath) ? tabFromPath : 'leaderboard';
}
$: if (selectedTab) {
// scroll to selectedTab
scrollToTab(selectedTab);
}
const scrollToTab = (tabId) => {
const tabElement = document.getElementById(tabId);
if (tabElement) {
tabElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
}
};
let loaded = false;
let feedbacks = [];
@@ -27,6 +46,9 @@
}
});
}
// Scroll to the selected tab on mount
scrollToTab(selectedTab);
});
</script>
@@ -37,12 +59,13 @@
class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
>
<button
id="leaderboard"
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
'leaderboard'
? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => {
selectedTab = 'leaderboard';
goto('/admin/evaluations/leaderboard');
}}
>
<div class=" self-center mr-2">
@@ -63,12 +86,13 @@
</button>
<button
id="feedbacks"
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
'feedbacks'
? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => {
selectedTab = 'feedbacks';
goto('/admin/evaluations/feedbacks');
}}
>
<div class=" self-center mr-2">
@@ -13,6 +13,9 @@
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';
@@ -30,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: {
@@ -53,6 +59,15 @@
lost: number;
};
function setSortKey(key) {
if (orderBy === key) {
direction = direction === 'asc' ? 'desc' : 'asc';
} else {
orderBy = key;
direction = key === 'name' ? 'asc' : 'desc';
}
}
//////////////////////
//
// Aggregate Level Modal
@@ -287,6 +302,28 @@
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
@@ -352,25 +389,120 @@
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)}
{#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)}
+28
View File
@@ -51,6 +51,18 @@
: 'general';
}
$: if (selectedTab) {
// scroll to selectedTab
scrollToTab(selectedTab);
}
const scrollToTab = (tabId) => {
const tabElement = document.getElementById(tabId);
if (tabElement) {
tabElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
}
};
onMount(() => {
const containerElement = document.getElementById('admin-settings-tabs-container');
@@ -62,6 +74,9 @@
}
});
}
// Scroll to the selected tab on mount
scrollToTab(selectedTab);
});
</script>
@@ -71,6 +86,7 @@
class="tabs flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
>
<button
id="general"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 lg:flex-none flex text-right transition {selectedTab ===
'general'
? ''
@@ -97,6 +113,7 @@
</button>
<button
id="connections"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'connections'
? ''
@@ -121,6 +138,7 @@
</button>
<button
id="models"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'models'
? ''
@@ -147,6 +165,7 @@
</button>
<button
id="evaluations"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'evaluations'
? ''
@@ -162,6 +181,7 @@
</button>
<button
id="tools"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'tools'
? ''
@@ -188,6 +208,7 @@
</button>
<button
id="documents"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'documents'
? ''
@@ -218,6 +239,7 @@
</button>
<button
id="web"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'web'
? ''
@@ -242,6 +264,7 @@
</button>
<button
id="code-execution"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'code-execution'
? ''
@@ -268,6 +291,7 @@
</button>
<button
id="interface"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'interface'
? ''
@@ -294,6 +318,7 @@
</button>
<button
id="audio"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'audio'
? ''
@@ -321,6 +346,7 @@
</button>
<button
id="images"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'images'
? ''
@@ -347,6 +373,7 @@
</button>
<button
id="pipelines"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'pipelines'
? ''
@@ -377,6 +404,7 @@
</button>
<button
id="db"
class="px-0.5 py-1 min-w-fit rounded-lg flex-1 md:flex-none flex text-left transition {selectedTab ===
'db'
? ''
@@ -194,17 +194,20 @@
await embeddingModelUpdateHandler();
}
RAGConfig.ALLOWED_FILE_EXTENSIONS = (RAGConfig?.ALLOWED_FILE_EXTENSIONS ?? '')
.split(',')
.map((ext) => ext.trim())
.filter((ext) => ext !== '');
RAGConfig.DATALAB_MARKER_LANGS = RAGConfig.DATALAB_MARKER_LANGS.split(',')
.map((code) => code.trim())
.filter((code) => code !== '')
.join(', ');
const res = await updateRAGConfig(localStorage.token, RAGConfig);
const res = await updateRAGConfig(localStorage.token, {
...RAGConfig,
ALLOWED_FILE_EXTENSIONS: RAGConfig.ALLOWED_FILE_EXTENSIONS.split(',')
.map((ext) => ext.trim())
.filter((ext) => ext !== ''),
DATALAB_MARKER_LANGS: RAGConfig.DATALAB_MARKER_LANGS.split(',')
.map((code) => code.trim())
.filter((code) => code !== '')
.join(', '),
DOCLING_PICTURE_DESCRIPTION_LOCAL: JSON.parse(
RAGConfig.DOCLING_PICTURE_DESCRIPTION_LOCAL || '{}'
),
DOCLING_PICTURE_DESCRIPTION_API: JSON.parse(RAGConfig.DOCLING_PICTURE_DESCRIPTION_API || '{}')
});
dispatch('save');
};
@@ -232,6 +235,18 @@
const config = await getRAGConfig(localStorage.token);
config.ALLOWED_FILE_EXTENSIONS = (config?.ALLOWED_FILE_EXTENSIONS ?? []).join(', ');
config.DOCLING_PICTURE_DESCRIPTION_LOCAL = JSON.stringify(
config.DOCLING_PICTURE_DESCRIPTION_LOCAL ?? {},
null,
2
);
config.DOCLING_PICTURE_DESCRIPTION_API = JSON.stringify(
config.DOCLING_PICTURE_DESCRIPTION_API ?? {},
null,
2
);
RAGConfig = config;
});
</script>
@@ -510,6 +525,71 @@
</div>
</div>
</div>
{#if RAGConfig.DOCLING_DO_PICTURE_DESCRIPTION}
<div class="flex justify-between w-full mt-2">
<div class="self-center text-xs font-medium">
<Tooltip content={''} placement="top-start">
{$i18n.t('Picture Description Mode')}
</Tooltip>
</div>
<div class="">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded-sm px-2 text-xs bg-transparent outline-hidden text-right"
bind:value={RAGConfig.DOCLING_PICTURE_DESCRIPTION_MODE}
>
<option value="">{$i18n.t('Default')}</option>
<option value="local">{$i18n.t('Local')}</option>
<option value="api">{$i18n.t('API')}</option>
</select>
</div>
</div>
{#if RAGConfig.DOCLING_PICTURE_DESCRIPTION_MODE === 'local'}
<div class="flex flex-col gap-2 mt-2">
<div class=" flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-medium">
{$i18n.t('Picture Description Local Config')}
</div>
<div class="flex w-full items-center relative">
<Tooltip
content={$i18n.t(
'Options for running a local vision-language model in the picture description. The parameters refer to a model hosted on Hugging Face. This parameter is mutually exclusive with picture_description_api.'
)}
placement="top-start"
className="w-full"
>
<Textarea
bind:value={RAGConfig.DOCLING_PICTURE_DESCRIPTION_LOCAL}
placeholder={$i18n.t('Enter Config in JSON format')}
/>
</Tooltip>
</div>
</div>
</div>
{:else if RAGConfig.DOCLING_PICTURE_DESCRIPTION_MODE === 'api'}
<div class="flex flex-col gap-2 mt-2">
<div class=" flex flex-col w-full justify-between">
<div class=" mb-1 text-xs font-medium">
{$i18n.t('Picture Description API Config')}
</div>
<div class="flex w-full items-center relative">
<Tooltip
content={$i18n.t(
'API details for using a vision-language model in the picture description. This parameter is mutually exclusive with picture_description_local.'
)}
placement="top-start"
className="w-full"
>
<Textarea
bind:value={RAGConfig.DOCLING_PICTURE_DESCRIPTION_API}
placeholder={$i18n.t('Enter Config in JSON format')}
/>
</Tooltip>
</div>
</div>
</div>
{/if}
{/if}
{:else if RAGConfig.CONTENT_EXTRACTION_ENGINE === 'document_intelligence'}
<div class="my-0.5 flex gap-2 pr-2">
<input
@@ -830,12 +910,7 @@
<div class=" mb-2.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Hybrid Search')}</div>
<div class="flex items-center relative">
<Switch
bind:state={RAGConfig.ENABLE_RAG_HYBRID_SEARCH}
on:change={() => {
submitHandler();
}}
/>
<Switch bind:state={RAGConfig.ENABLE_RAG_HYBRID_SEARCH} />
</div>
</div>
@@ -79,6 +79,7 @@
const updateHandler = async () => {
webhookUrl = await updateWebhookUrl(localStorage.token, webhookUrl);
const res = await updateAdminConfig(localStorage.token, adminConfig);
await updateLdapConfig(localStorage.token, ENABLE_LDAP);
await updateLdapServerHandler();
if (res) {
@@ -311,7 +312,6 @@
{$i18n.t('Pending User Overlay Title')}
</div>
<Textarea
rows={2}
placeholder={$i18n.t(
'Enter a title for the pending user info overlay. Leave empty for default.'
)}
@@ -401,12 +401,7 @@
<div class=" font-medium">{$i18n.t('LDAP')}</div>
<div class="mt-1">
<Switch
bind:state={ENABLE_LDAP}
on:change={async () => {
updateLdapConfig(localStorage.token, ENABLE_LDAP);
}}
/>
<Switch bind:state={ENABLE_LDAP} />
</div>
</div>
@@ -75,6 +75,8 @@
};
const init = async () => {
models = null;
workspaceModels = await getBaseModels(localStorage.token);
baseModels = await getModels(localStorage.token, null, true);
@@ -126,6 +128,7 @@
toast.success($i18n.t('Model updated successfully'));
}
}
await init();
_models.set(
await getModels(
@@ -133,7 +136,6 @@
$config?.features?.enable_direct_connections && ($settings?.directConnections ?? null)
)
);
await init();
};
const toggleModelHandler = async (model) => {
@@ -45,7 +45,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"
@@ -446,15 +446,54 @@
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity'}
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Perplexity API Key')}
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Perplexity API Key')}
</div>
<SensitiveInput
placeholder={$i18n.t('Enter Perplexity API Key')}
bind:value={webConfig.PERPLEXITY_API_KEY}
/>
<SensitiveInput
placeholder={$i18n.t('Enter Perplexity API Key')}
bind:value={webConfig.PERPLEXITY_API_KEY}
/>
</div>
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class="self-center text-xs font-medium mb-1">
{$i18n.t('Perplexity Model')}
</div>
<input
list="perplexity-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={webConfig.PERPLEXITY_MODEL}
/>
<datalist id="perplexity-model-list">
<option value="sonar">Sonar</option>
<option value="sonar-pro">Sonar Pro</option>
<option value="sonar-reasoning">Sonar Reasoning</option>
<option value="sonar-reasoning-pro">Sonar Reasoning Pro</option>
<option value="sonar-deep-research">Sonar Deep Research</option>
</datalist>
</div>
</div>
<div class="mb-2.5 flex w-full flex-col">
<div>
<div class=" self-center text-xs font-medium mb-1">
{$i18n.t('Perplexity Search Context Usage')}
</div>
<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={webConfig.PERPLEXITY_SEARCH_CONTEXT_USAGE}
>
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
</div>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'sougou'}
<div class="mb-2.5 flex w-full flex-col">
+27 -3
View File
@@ -4,13 +4,32 @@
import { goto } from '$app/navigation';
import { user } from '$lib/stores';
import { page } from '$app/stores';
import UserList from './Users/UserList.svelte';
import Groups from './Users/Groups.svelte';
const i18n = getContext('i18n');
let selectedTab = 'overview';
let selectedTab;
$: {
const pathParts = $page.url.pathname.split('/');
const tabFromPath = pathParts[pathParts.length - 1];
selectedTab = ['overview', 'groups'].includes(tabFromPath) ? tabFromPath : 'overview';
}
$: if (selectedTab) {
// scroll to selectedTab
scrollToTab(selectedTab);
}
const scrollToTab = (tabId) => {
const tabElement = document.getElementById(tabId);
if (tabElement) {
tabElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
}
};
let loaded = false;
onMount(async () => {
@@ -30,6 +49,9 @@
}
});
}
// Scroll to the selected tab on mount
scrollToTab(selectedTab);
});
</script>
@@ -39,12 +61,13 @@
class=" flex flex-row overflow-x-auto gap-2.5 max-w-full lg:gap-1 lg:flex-col lg:flex-none lg:w-40 dark:text-gray-200 text-sm font-medium text-left scrollbar-none"
>
<button
id="overview"
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
'overview'
? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => {
selectedTab = 'overview';
goto('/admin/users/overview');
}}
>
<div class=" self-center mr-2">
@@ -63,12 +86,13 @@
</button>
<button
id="groups"
class="px-0.5 py-1 min-w-fit rounded-lg lg:flex-none flex text-right transition {selectedTab ===
'groups'
? ''
: ' text-gray-300 dark:text-gray-600 hover:text-gray-700 dark:hover:text-white'}"
on:click={() => {
selectedTab = 'groups';
goto('/admin/users/groups');
}}
>
<div class=" self-center mr-2">
@@ -101,7 +101,7 @@
<div class="flex-1">
<select
class="w-full rounded-sm text-sm bg-transparent disabled:text-gray-500 dark:disabled:text-gray-500 outline-hidden"
class="w-full dark:bg-gray-900 rounded-sm 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
+7 -14
View File
@@ -432,24 +432,19 @@
}
};
let pageSubscribe = null;
onMount(async () => {
loading = true;
console.log('mounted');
window.addEventListener('message', onMessageHandler);
$socket?.on('chat-events', chatEventHandler);
if (!$chatId) {
chatIdUnsubscriber = chatId.subscribe(async (value) => {
if (!value) {
await tick(); // Wait for DOM updates
await initNewChat();
}
});
} else {
if ($temporaryChatEnabled) {
await goto('/');
pageSubscribe = page.subscribe(async (p) => {
if (p.url.pathname === '/') {
await tick();
initNewChat();
}
}
});
if (localStorage.getItem(`chat-input${chatIdProp ? `-${chatIdProp}` : ''}`)) {
prompt = '';
@@ -509,6 +504,7 @@
});
onDestroy(() => {
pageSubscribe();
chatIdUnsubscriber?.();
window.removeEventListener('message', onMessageHandler);
$socket?.off('chat-events', chatEventHandler);
@@ -1636,9 +1632,6 @@
params: {
...$settings?.params,
...params,
format: $settings.requestFormat ?? undefined,
keep_alive: $settings.keepAlive ?? undefined,
stop:
(params?.stop ?? $settings?.params?.stop ?? undefined)
? (params?.stop.split(',').map((token) => token.trim()) ?? $settings.params.stop).map(
@@ -69,10 +69,10 @@
{#if $temporaryChatEnabled}
<Tooltip
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"
className="w-full flex justify-start mb-0.5"
placement="top"
>
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg my-2 w-fit">
<div class="flex items-center gap-2 text-gray-500 font-medium text-lg mt-2 w-fit">
<EyeSlash strokeWidth="2.5" className="size-5" />{$i18n.t('Temporary Chat')}
</div>
</Tooltip>
@@ -190,7 +190,7 @@
</div>
{#if selectedId}
<hr class="dark:border-gray-800 my-1 w-full" />
<hr class="border-gray-50 dark:border-gray-800 my-1 w-full" />
<div class="my-2 text-xs">
{#if !loading}
@@ -205,8 +205,10 @@
return;
}
const mineTypes = ['audio/webm; codecs=opus', 'audio/mp4'];
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm; codecs=opus'
mimeType: mineTypes.find((type) => MediaRecorder.isTypeSupported(type))
});
mediaRecorder.onstart = () => {
@@ -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">
@@ -28,7 +28,7 @@
<!-- svelte-ignore a11y-media-has-caption -->
<video
class="w-full my-2"
src={videoSrc}
src={videoSrc.replaceAll('&amp;', '&')}
title="Video player"
frameborder="0"
referrerpolicy="strict-origin-when-cross-origin"
@@ -38,6 +38,20 @@
{:else}
{token.text}
{/if}
{:else if html && html.includes('<audio')}
{@const audio = html.match(/<audio[^>]*>([\s\S]*?)<\/audio>/)}
{@const audioSrc = audio && audio[1]}
{#if audioSrc}
<!-- svelte-ignore a11y-media-has-caption -->
<audio
class="w-full my-2"
src={audioSrc.replaceAll('&amp;', '&')}
title="Audio player"
controls
></audio>
{:else}
{token.text}
{/if}
{:else if token.text && token.text.match(/<iframe\s+[^>]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?:\?[^"]*)?"[^>]*><\/iframe>/)}
{@const match = token.text.match(
/<iframe\s+[^>]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]{11})(?:\?[^"]*)?"[^>]*><\/iframe>/
@@ -137,8 +137,8 @@
</div>
<div class="w-full flex justify-center">
<div class=" relative w-fit">
<div class="mt-1.5 w-fit flex gap-1 pb-5">
<div class=" relative w-fit overflow-x-auto scrollbar-none">
<div class="mt-1.5 w-fit flex gap-1 pb-2">
<!-- 1-10 scale -->
{#each Array.from({ length: 10 }).map((_, i) => i + 1) as rating}
<button
@@ -156,7 +156,7 @@
{/each}
</div>
<div class="absolute bottom-0 left-0 right-0 flex justify-between text-xs">
<div class="sticky top-0 bottom-0 left-0 right-0 flex justify-between text-xs">
<div>
1 - {$i18n.t('Awful')}
</div>
@@ -601,7 +601,7 @@
id="message-{message.id}"
dir={$settings.chatDirection}
>
<div class={`shrink-0 ltr:mr-3 rtl:ml-3`}>
<div class={`shrink-0 ltr:mr-3 rtl:ml-3 hidden @lg:flex `}>
<ProfileImage
src={model?.info?.meta?.profile_image_url ??
($i18n.language === 'dg-DG' ? `/doge.png` : `${WEBUI_BASE_URL}/static/favicon.png`)}
@@ -869,12 +869,14 @@
{#if siblings.length > 1}
<div class="flex self-center min-w-fit" dir="ltr">
<button
aria-label={$i18n.t('Previous message')}
class="self-center p-1 hover:bg-black/5 dark:hover:bg-white/5 dark:hover:text-white hover:text-black rounded-md transition"
on:click={() => {
showPreviousMessage(message);
}}
>
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -940,10 +942,12 @@
on:click={() => {
showNextMessage(message);
}}
aria-label={$i18n.t('Next message')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
aria-hidden="true"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2.5"
@@ -964,6 +968,7 @@
{#if $user?.role === 'user' ? ($user?.permissions?.chat?.edit ?? true) : true}
<Tooltip content={$i18n.t('Edit')} placement="bottom">
<button
aria-label={$i18n.t('Edit')}
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
@@ -976,6 +981,7 @@
fill="none"
viewBox="0 0 24 24"
stroke-width="2.3"
aria-hidden="true"
stroke="currentColor"
class="w-4 h-4"
>
@@ -992,6 +998,7 @@
<Tooltip content={$i18n.t('Copy')} placement="bottom">
<button
aria-label={$i18n.t('Copy')}
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition copy-response-button"
@@ -1002,6 +1009,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
aria-hidden="true"
viewBox="0 0 24 24"
stroke-width="2.3"
stroke="currentColor"
@@ -1019,6 +1027,7 @@
{#if $user?.role === 'admin' || ($user?.permissions?.chat?.tts ?? true)}
<Tooltip content={$i18n.t('Read Aloud')} placement="bottom">
<button
aria-label={$i18n.t('Read Aloud')}
id="speak-button-{message.id}"
class="{isLastMessage
? 'visible'
@@ -1034,6 +1043,7 @@
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
>
<style>
@@ -1066,6 +1076,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
@@ -1081,6 +1092,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
stroke-width="2.3"
stroke="currentColor"
class="w-4 h-4"
@@ -1099,6 +1111,7 @@
{#if $config?.features.enable_image_generation && ($user?.role === 'admin' || $user?.permissions?.features?.image_generation) && !readOnly}
<Tooltip content={$i18n.t('Generate Image')} placement="bottom">
<button
aria-label={$i18n.t('Generate Image')}
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
@@ -1110,6 +1123,7 @@
>
{#if generatingImage}
<svg
aria-hidden="true"
class=" w-4 h-4"
fill="currentColor"
viewBox="0 0 24 24"
@@ -1144,6 +1158,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
aria-hidden="true"
viewBox="0 0 24 24"
stroke-width="2.3"
stroke="currentColor"
@@ -1176,6 +1191,7 @@
placement="bottom"
>
<button
aria-hidden="true"
class=" {isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition whitespace-pre-wrap"
@@ -1185,6 +1201,7 @@
id="info-{message.id}"
>
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -1206,6 +1223,7 @@
{#if !$temporaryChatEnabled && ($config?.features.enable_message_rating ?? true)}
<Tooltip content={$i18n.t('Good Response')} placement="bottom">
<button
aria-label={$i18n.t('Good Response')}
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(
@@ -1224,6 +1242,7 @@
}}
>
<svg
aria-hidden="true"
stroke="currentColor"
fill="none"
stroke-width="2.3"
@@ -1242,6 +1261,7 @@
<Tooltip content={$i18n.t('Bad Response')} placement="bottom">
<button
aria-label={$i18n.t('Bad Response')}
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg {(
@@ -1260,6 +1280,7 @@
}}
>
<svg
aria-hidden="true"
stroke="currentColor"
fill="none"
stroke-width="2.3"
@@ -1280,6 +1301,7 @@
{#if isLastMessage}
<Tooltip content={$i18n.t('Continue Response')} placement="bottom">
<button
aria-label={$i18n.t('Continue Response')}
type="button"
id="continue-response-button"
class="{isLastMessage
@@ -1290,6 +1312,7 @@
}}
>
<svg
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -1315,6 +1338,7 @@
<Tooltip content={$i18n.t('Regenerate')} placement="bottom">
<button
type="button"
aria-label={$i18n.t('Regenerate')}
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition regenerate-response-button"
@@ -1340,6 +1364,7 @@
fill="none"
viewBox="0 0 24 24"
stroke-width="2.3"
aria-hidden="true"
stroke="currentColor"
class="w-4 h-4"
>
@@ -1356,6 +1381,7 @@
<Tooltip content={$i18n.t('Delete')} placement="bottom">
<button
type="button"
aria-label={$i18n.t('Delete')}
id="delete-response-button"
class="{isLastMessage
? 'visible'
@@ -1370,6 +1396,7 @@
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
aria-hidden="true"
class="w-4 h-4"
>
<path
@@ -1387,6 +1414,7 @@
<Tooltip content={action.name} placement="bottom">
<button
type="button"
aria-label={action.name}
class="{isLastMessage
? 'visible'
: 'invisible group-hover:visible'} p-1.5 hover:bg-black/5 dark:hover:bg-white/5 rounded-lg dark:hover:text-white hover:text-black transition"
@@ -25,6 +25,19 @@
toast.success($i18n.t('Default model updated'));
};
const pinModelHandler = async (modelId) => {
let pinnedModels = $settings?.pinnedModels ?? [];
if (pinnedModels.includes(modelId)) {
pinnedModels = pinnedModels.filter((id) => id !== modelId);
} else {
pinnedModels = [...new Set([...pinnedModels, modelId])];
}
settings.set({ ...$settings, pinnedModels: pinnedModels });
await updateUserSettings(localStorage.token, { ui: $settings });
};
$: if (selectedModels.length > 0 && $models.length > 0) {
selectedModels = selectedModels.map((model) =>
$models.map((m) => m.id).includes(model) ? model : ''
@@ -49,6 +62,7 @@
? ($user?.permissions?.chat?.temporary ?? true) &&
!($user?.permissions?.chat?.temporary_enforced ?? false)
: true}
{pinModelHandler}
bind:value={selectedModel}
/>
</div>
@@ -0,0 +1,250 @@
<script lang="ts">
import { marked } from 'marked';
import { getContext, tick } from 'svelte';
import dayjs from '$lib/dayjs';
import { mobile, settings, user } from '$lib/stores';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { copyToClipboard, sanitizeResponseContent } from '$lib/utils';
import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
import Check from '$lib/components/icons/Check.svelte';
import ModelItemMenu from './ModelItemMenu.svelte';
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
import { toast } from 'svelte-sonner';
const i18n = getContext('i18n');
export let selectedModelIdx: number = -1;
export let item: any = {};
export let index: number = -1;
export let value: string = '';
export let unloadModelHandler: (modelValue: string) => void = () => {};
export let pinModelHandler: (modelId: string) => void = () => {};
export let onClick: () => void = () => {};
const copyLinkHandler = async (model) => {
const baseUrl = window.location.origin;
const res = await copyToClipboard(`${baseUrl}/?model=${encodeURIComponent(model.id)}`);
if (res) {
toast.success($i18n.t('Copied link to clipboard'));
} else {
toast.error($i18n.t('Failed to copy link'));
}
};
let showMenu = false;
</script>
<button
aria-label="model-item"
class="flex group/item w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
selectedModelIdx
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
: ''}"
data-arrow-selected={index === selectedModelIdx}
data-value={item.value}
on:click={() => {
onClick();
}}
>
<div class="flex flex-col flex-1 gap-1.5">
{#if (item?.model?.tags ?? []).length > 0}
<div
class="flex gap-0.5 self-center items-start h-full w-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
>
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
<Tooltip content={tag.name} className="flex-shrink-0">
<div
class=" text-xs font-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
</Tooltip>
{/each}
</div>
{/if}
<div class="flex items-center gap-2">
<div class="flex items-center min-w-fit">
<Tooltip content={$user?.role === 'admin' ? (item?.value ?? '') : ''} placement="top-start">
<img
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
alt="Model"
class="rounded-full size-5 flex items-center"
/>
</Tooltip>
</div>
<div class="flex items-center">
<Tooltip content={`${item.label} (${item.value})`} placement="top-start">
<div class="line-clamp-1">
{item.label}
</div>
</Tooltip>
</div>
<div class=" shrink-0 flex items-center gap-2">
{#if item.model.owned_by === 'ollama'}
{#if (item.model.ollama?.details?.parameter_size ?? '') !== ''}
<div class="flex items-center translate-y-[0.5px]">
<Tooltip
content={`${
item.model.ollama?.details?.quantization_level
? item.model.ollama?.details?.quantization_level + ' '
: ''
}${
item.model.ollama?.size
? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
: ''
}`}
className="self-end"
>
<span class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
>{item.model.ollama?.details?.parameter_size ?? ''}</span
>
</Tooltip>
</div>
{/if}
{#if item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
<div class="flex items-center translate-y-[0.5px] px-0.5">
<Tooltip
content={`${$i18n.t('Unloads {{FROM_NOW}}', {
FROM_NOW: dayjs(item.model.ollama?.expires_at * 1000).fromNow()
})}`}
className="self-end"
>
<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>
</div>
{/if}
{/if}
<!-- {JSON.stringify(item.info)} -->
{#if item.model?.direct}
<Tooltip content={`${$i18n.t('Direct')}`}>
<div class="translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
clip-rule="evenodd"
/>
</svg>
</div>
</Tooltip>
{:else if item.model.connection_type === 'external'}
<Tooltip content={`${$i18n.t('External')}`}>
<div class="translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
</Tooltip>
{/if}
{#if item.model?.info?.meta?.description}
<Tooltip
content={`${marked.parse(
sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll('\n', '<br>')
)}`}
>
<div class=" translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</div>
</Tooltip>
{/if}
</div>
</div>
</div>
<div class="ml-auto pl-2 pr-1 flex items-center gap-1.5 shrink-0">
{#if $user?.role === 'admin' && item.model.owned_by === 'ollama' && item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
<Tooltip
content={`${$i18n.t('Eject')}`}
className="flex-shrink-0 group-hover/item:opacity-100 opacity-0 "
>
<button
class="flex"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
unloadModelHandler(item.value);
}}
>
<ArrowUpTray className="size-3" />
</button>
</Tooltip>
{/if}
<ModelItemMenu
bind:show={showMenu}
model={item.model}
{pinModelHandler}
copyLinkHandler={() => {
copyLinkHandler(item.model);
}}
>
<button
class="flex"
on:click={(e) => {
e.preventDefault();
e.stopPropagation();
showMenu = !showMenu;
}}
>
<EllipsisHorizontal />
</button>
</ModelItemMenu>
{#if value === item.value}
<div>
<Check className="size-3" />
</div>
{/if}
</div>
</button>
@@ -0,0 +1,90 @@
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { flyAndScale } from '$lib/utils/transitions';
import { getContext } from 'svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Link from '$lib/components/icons/Link.svelte';
import Eye from '$lib/components/icons/Eye.svelte';
import EyeSlash from '$lib/components/icons/EyeSlash.svelte';
import { settings } from '$lib/stores';
const i18n = getContext('i18n');
export let show = false;
export let model;
export let pinModelHandler: (modelId: string) => void = () => {};
export let copyLinkHandler: Function = () => {};
export let onClose: Function = () => {};
</script>
<DropdownMenu.Root
bind:open={show}
closeFocus={false}
onOpenChange={(state) => {
if (state === false) {
onClose();
}
}}
typeahead={false}
>
<DropdownMenu.Trigger>
<Tooltip content={$i18n.t('More')} className=" group-hover/item:opacity-100 opacity-0">
<slot />
</Tooltip>
</DropdownMenu.Trigger>
<DropdownMenu.Content
strategy="fixed"
class="w-full max-w-[180px] text-sm rounded-xl px-1 py-1.5 z-[9999999] bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={-2}
side="bottom"
align="end"
transition={flyAndScale}
>
<button
type="button"
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
pinModelHandler(model?.id);
show = false;
}}
>
{#if ($settings?.pinnedModels ?? []).includes(model?.id)}
<EyeSlash />
{:else}
<Eye />
{/if}
<div class="flex items-center">
{#if ($settings?.pinnedModels ?? []).includes(model?.id)}
{$i18n.t('Hide from Sidebar')}
{:else}
{$i18n.t('Keep in Sidebar')}
{/if}
</div>
</button>
<button
type="button"
class="flex rounded-md py-1.5 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition items-center gap-2"
on:click={(e) => {
e.stopPropagation();
e.preventDefault();
copyLinkHandler();
show = false;
}}
>
<Link />
<div class="flex items-center">{$i18n.t('Copy Link')}</div>
</button>
</DropdownMenu.Content>
</DropdownMenu.Root>
@@ -3,12 +3,13 @@
import { marked } from 'marked';
import Fuse from 'fuse.js';
import dayjs from '$lib/dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
import { flyAndScale } from '$lib/utils/transitions';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import Search from '$lib/components/icons/Search.svelte';
import { goto } from '$app/navigation';
import { deleteModel, getOllamaVersion, pullModel, unloadModel } from '$lib/apis/ollama';
@@ -25,14 +26,14 @@
import { capitalizeFirstLetter, sanitizeResponseContent, splitStream } from '$lib/utils';
import { getModels } from '$lib/apis';
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
import Check from '$lib/components/icons/Check.svelte';
import Search from '$lib/components/icons/Search.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Switch from '$lib/components/common/Switch.svelte';
import ChatBubbleOval from '$lib/components/icons/ChatBubbleOval.svelte';
import { goto } from '$app/navigation';
import dayjs from '$lib/dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import ArrowUpTray from '$lib/components/icons/ArrowUpTray.svelte';
dayjs.extend(relativeTime);
import ModelItem from './ModelItem.svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
@@ -56,6 +57,8 @@
export let className = 'w-[32rem]';
export let triggerClassName = 'text-lg';
export let pinModelHandler: (modelId: string) => void = () => {};
let tagsContainerElement;
let show = false;
@@ -407,10 +410,10 @@
</div>
{/if}
<div class="px-3 max-h-64 overflow-y-auto scrollbar-hidden group relative">
<div class="px-3">
{#if tags && items.filter((item) => !(item.model?.info?.meta?.hidden ?? false)).length > 0}
<div
class=" flex w-full sticky top-0 z-10 bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none"
class=" flex w-full bg-white dark:bg-gray-850 overflow-x-auto scrollbar-none"
on:wheel={(e) => {
if (e.deltaY !== 0) {
e.preventDefault();
@@ -492,212 +495,24 @@
</div>
</div>
{/if}
</div>
<div class="px-3 max-h-64 overflow-y-auto group relative">
{#each filteredItems as item, index}
<button
aria-label="model-item"
class="flex w-full text-left font-medium line-clamp-1 select-none items-center rounded-button py-2 pl-3 pr-1.5 text-sm text-gray-700 dark:text-gray-100 outline-hidden transition-all duration-75 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg cursor-pointer data-highlighted:bg-muted {index ===
selectedModelIdx
? 'bg-gray-100 dark:bg-gray-800 group-hover:bg-transparent'
: ''}"
data-arrow-selected={index === selectedModelIdx}
data-value={item.value}
on:click={() => {
<ModelItem
{selectedModelIdx}
{item}
{index}
{value}
{pinModelHandler}
{unloadModelHandler}
onClick={() => {
value = item.value;
selectedModelIdx = index;
show = false;
}}
>
<div class="flex flex-col">
{#if $mobile && (item?.model?.tags ?? []).length > 0}
<div class="flex gap-0.5 self-start h-full mb-1.5 -translate-x-1">
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
<div
class=" text-xs font-bold px-1 rounded-sm uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
{/each}
</div>
{/if}
<div class="flex items-center gap-2">
<div class="flex items-center min-w-fit">
<div class="line-clamp-1">
<div class="flex items-center min-w-fit">
<Tooltip
content={$user?.role === 'admin' ? (item?.value ?? '') : ''}
placement="top-start"
>
<img
src={item.model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
alt="Model"
class="rounded-full size-5 flex items-center mr-2"
/>
<div class="flex items-center line-clamp-1">
<div class="line-clamp-1">
{item.label}
</div>
</div>
</Tooltip>
</div>
</div>
</div>
{#if item.model.owned_by === 'ollama'}
{#if (item.model.ollama?.details?.parameter_size ?? '') !== ''}
<div class="flex items-center translate-y-[0.5px]">
<Tooltip
content={`${
item.model.ollama?.details?.quantization_level
? item.model.ollama?.details?.quantization_level + ' '
: ''
}${
item.model.ollama?.size
? `(${(item.model.ollama?.size / 1024 ** 3).toFixed(1)}GB)`
: ''
}`}
className="self-end"
>
<span
class=" text-xs font-medium text-gray-600 dark:text-gray-400 line-clamp-1"
>{item.model.ollama?.details?.parameter_size ?? ''}</span
>
</Tooltip>
</div>
{/if}
{#if item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
<div class="flex items-center translate-y-[0.5px] px-0.5">
<Tooltip
content={`${$i18n.t('Unloads {{FROM_NOW}}', {
FROM_NOW: dayjs(item.model.ollama?.expires_at * 1000).fromNow()
})}`}
className="self-end"
>
<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>
</div>
{/if}
{/if}
<!-- {JSON.stringify(item.info)} -->
{#if item.model?.direct}
<Tooltip content={`${$i18n.t('Direct')}`}>
<div class="translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M2 2.75A.75.75 0 0 1 2.75 2C8.963 2 14 7.037 14 13.25a.75.75 0 0 1-1.5 0c0-5.385-4.365-9.75-9.75-9.75A.75.75 0 0 1 2 2.75Zm0 4.5a.75.75 0 0 1 .75-.75 6.75 6.75 0 0 1 6.75 6.75.75.75 0 0 1-1.5 0C8 10.35 5.65 8 2.75 8A.75.75 0 0 1 2 7.25ZM3.5 11a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
clip-rule="evenodd"
/>
</svg>
</div>
</Tooltip>
{:else if item.model.connection_type === 'external'}
<Tooltip content={`${$i18n.t('External')}`}>
<div class="translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="size-3"
>
<path
fill-rule="evenodd"
d="M8.914 6.025a.75.75 0 0 1 1.06 0 3.5 3.5 0 0 1 0 4.95l-2 2a3.5 3.5 0 0 1-5.396-4.402.75.75 0 0 1 1.251.827 2 2 0 0 0 3.085 2.514l2-2a2 2 0 0 0 0-2.828.75.75 0 0 1 0-1.06Z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M7.086 9.975a.75.75 0 0 1-1.06 0 3.5 3.5 0 0 1 0-4.95l2-2a3.5 3.5 0 0 1 5.396 4.402.75.75 0 0 1-1.251-.827 2 2 0 0 0-3.085-2.514l-2 2a2 2 0 0 0 0 2.828.75.75 0 0 1 0 1.06Z"
clip-rule="evenodd"
/>
</svg>
</div>
</Tooltip>
{/if}
{#if item.model?.info?.meta?.description}
<Tooltip
content={`${marked.parse(
sanitizeResponseContent(item.model?.info?.meta?.description).replaceAll(
'\n',
'<br>'
)
)}`}
>
<div class=" translate-y-[1px]">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
</div>
</Tooltip>
{/if}
{#if !$mobile && (item?.model?.tags ?? []).length > 0}
<div
class="flex gap-0.5 self-center items-center h-full translate-y-[0.5px] overflow-x-auto scrollbar-none"
>
{#each item.model?.tags.sort((a, b) => a.name.localeCompare(b.name)) as tag}
<Tooltip content={tag.name} className="flex-shrink-0">
<div
class=" text-xs font-bold px-1 rounded-sm uppercase bg-gray-500/20 text-gray-700 dark:text-gray-200"
>
{tag.name}
</div>
</Tooltip>
{/each}
</div>
{/if}
</div>
</div>
<div class="ml-auto pl-2 pr-1 flex gap-1.5 items-center">
{#if $user?.role === 'admin' && item.model.owned_by === 'ollama' && item.model.ollama?.expires_at && new Date(item.model.ollama?.expires_at * 1000) > new Date()}
<Tooltip content={`${$i18n.t('Eject')}`} className="flex-shrink-0">
<button
class="flex"
on:click={() => {
unloadModelHandler(item.value);
}}
>
<ArrowUpTray className="size-3" />
</button>
</Tooltip>
{/if}
{#if value === item.value}
<div>
<Check className="size-3" />
</div>
{/if}
</div>
</button>
/>
{:else}
<div class="">
<div class="block px-3 py-2 text-sm text-gray-700 dark:text-gray-100">
+1 -1
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"
>
@@ -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"
@@ -1,5 +1,6 @@
<script lang="ts">
import Switch from '$lib/components/common/Switch.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Plus from '$lib/components/icons/Plus.svelte';
import { getContext } from 'svelte';
@@ -34,6 +35,9 @@
repeat_penalty: null,
use_mmap: null,
use_mlock: null,
think: null,
format: null,
keep_alive: null,
num_keep: null,
num_ctx: null,
num_batch: null,
@@ -87,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"
@@ -1092,6 +1096,74 @@
</div>
{/if}
<div class=" py-0.5 w-full justify-between">
<Tooltip
content={$i18n.t(
'This option enables or disables the use of the reasoning feature in Ollama, which allows the model to think before generating a response. When enabled, the model can take a moment to process the conversation context and generate a more thoughtful response.'
)}
placement="top-start"
className="inline-tooltip"
>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{'think'} ({$i18n.t('Ollama')})
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
params.think = (params?.think ?? null) === null ? true : params.think ? false : null;
}}
type="button"
>
{#if params.think === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else if params.think === false}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{/if}
</button>
</div>
</Tooltip>
</div>
<div class=" py-0.5 w-full justify-between">
<Tooltip
content={$i18n.t('The format to return a response in. Format can be json or a JSON schema.')}
placement="top-start"
className="inline-tooltip"
>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{'format'} ({$i18n.t('Ollama')})
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
params.format = (params?.format ?? null) === null ? 'json' : null;
}}
type="button"
>
{#if (params?.format ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('JSON')}</span>
{/if}
</button>
</div>
</Tooltip>
{#if (params?.format ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<Textarea
className="w-full text-sm bg-transparent outline-hidden"
placeholder={$i18n.t('e.g. "json" or a JSON schema')}
bind:value={params.format}
/>
</div>
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<Tooltip
content={$i18n.t(
@@ -1368,6 +1440,46 @@
{/if}
</div>
<div class=" py-0.5 w-full justify-between">
<Tooltip
content={$i18n.t(
'This option controls how long the model will stay loaded into memory following the request (default: 5m)'
)}
placement="top-start"
className="inline-tooltip"
>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{'keep_alive'} ({$i18n.t('Ollama')})
</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
params.keep_alive = (params?.keep_alive ?? null) === null ? '5m' : null;
}}
type="button"
>
{#if (params?.keep_alive ?? null) === null}
<span class="ml-2 self-center">{$i18n.t('Default')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Custom')}</span>
{/if}
</button>
</div>
</Tooltip>
{#if (params?.keep_alive ?? null) !== null}
<div class="flex mt-0.5 space-x-2">
<input
class="w-full text-sm bg-transparent outline-hidden"
type="text"
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
bind:value={params.keep_alive}
/>
</div>
{/if}
</div>
{#if custom && admin}
<div class="flex flex-col justify-center">
{#each Object.keys(params?.custom_params ?? {}) as key}
@@ -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(
+7 -123
View File
@@ -10,12 +10,11 @@
import AdvancedParams from './Advanced/AdvancedParams.svelte';
import Textarea from '$lib/components/common/Textarea.svelte';
export let saveSettings: Function;
export let getModels: Function;
// General
let themes = ['dark', 'light', 'rose-pine dark', 'rose-pine-dawn light', 'oled-dark'];
let themes = ['dark', 'light', 'oled-dark'];
let selectedTheme = 'system';
let languages: Awaited<ReturnType<typeof getLanguages>> = [];
@@ -40,10 +39,6 @@
}
};
// Advanced
let requestFormat = null;
let keepAlive: string | null = null;
let params = {
// Advanced
stream_response: null,
@@ -71,37 +66,7 @@
num_gpu: null
};
const validateJSON = (json) => {
try {
const obj = JSON.parse(json);
if (obj && typeof obj === 'object') {
return true;
}
} catch (e) {}
return false;
};
const toggleRequestFormat = async () => {
if (requestFormat === null) {
requestFormat = 'json';
} else {
requestFormat = null;
}
saveSettings({ requestFormat: requestFormat !== null ? requestFormat : undefined });
};
const saveHandler = async () => {
if (requestFormat !== null && requestFormat !== 'json') {
if (validateJSON(requestFormat) === false) {
toast.error($i18n.t('Invalid JSON schema'));
return;
} else {
requestFormat = JSON.parse(requestFormat);
}
}
saveSettings({
system: system !== '' ? system : undefined,
params: {
@@ -130,15 +95,13 @@
use_mmap: params.use_mmap !== null ? params.use_mmap : undefined,
use_mlock: params.use_mlock !== null ? params.use_mlock : undefined,
num_thread: params.num_thread !== null ? params.num_thread : undefined,
num_gpu: params.num_gpu !== null ? params.num_gpu : undefined
},
keepAlive: keepAlive ? (isNaN(keepAlive) ? keepAlive : parseInt(keepAlive)) : undefined,
requestFormat: requestFormat !== null ? requestFormat : undefined
num_gpu: params.num_gpu !== null ? params.num_gpu : undefined,
think: params.think !== null ? params.think : undefined,
keep_alive: params.keep_alive !== null ? params.keep_alive : undefined,
format: params.format !== null ? params.format : undefined
}
});
dispatch('save');
requestFormat =
typeof requestFormat === 'object' ? JSON.stringify(requestFormat, null, 2) : requestFormat;
};
onMount(async () => {
@@ -149,14 +112,6 @@
notificationEnabled = $settings.notificationEnabled ?? false;
system = $settings.system ?? '';
requestFormat = $settings.requestFormat ?? null;
if (requestFormat !== null && requestFormat !== 'json') {
requestFormat =
typeof requestFormat === 'object' ? JSON.stringify(requestFormat, null, 2) : requestFormat;
}
keepAlive = $settings.keepAlive ?? null;
params = { ...params, ...$settings.params };
params.stop = $settings?.params?.stop ? ($settings?.params?.stop ?? []).join(',') : null;
});
@@ -232,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>
@@ -335,77 +290,6 @@
{#if showAdvanced}
<AdvancedParams admin={$user?.role === 'admin'} bind:params />
<hr class=" border-gray-100 dark:border-gray-850" />
<div class=" w-full justify-between">
<div class="flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Keep Alive')}</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
type="button"
on:click={() => {
keepAlive = keepAlive === null ? '5m' : null;
}}
>
{#if keepAlive === null}
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else}
<span class="ml-2 self-center"> {$i18n.t('Custom')} </span>
{/if}
</button>
</div>
{#if keepAlive !== null}
<div class="flex mt-1 space-x-2">
<input
class="w-full text-sm dark:text-gray-300 dark:bg-gray-850 outline-hidden"
type="text"
placeholder={$i18n.t("e.g. '30s','10m'. Valid time units are 's', 'm', 'h'.")}
bind:value={keepAlive}
/>
</div>
{/if}
</div>
<div>
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Request Mode')}</div>
<button
class="p-1 px-3 text-xs flex rounded-sm transition"
on:click={() => {
toggleRequestFormat();
}}
>
{#if requestFormat === null}
<span class="ml-2 self-center"> {$i18n.t('Default')} </span>
{:else}
<!-- <svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4 self-center"
>
<path
d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.061 1.06l1.06 1.06z"
/>
</svg> -->
<span class="ml-2 self-center"> {$i18n.t('JSON')} </span>
{/if}
</button>
</div>
{#if requestFormat !== null}
<div class="flex mt-1 space-x-2">
<Textarea
className="w-full text-sm dark:text-gray-300 dark:bg-gray-900 outline-hidden"
placeholder={$i18n.t('e.g. "json" or a JSON schema')}
bind:value={requestFormat}
/>
</div>
{/if}
</div>
{/if}
</div>
{/if}
@@ -292,36 +292,36 @@
onMount(async () => {
titleAutoGenerate = $settings?.title?.auto ?? true;
autoTags = $settings.autoTags ?? true;
autoFollowUps = $settings.autoFollowUps ?? true;
autoTags = $settings?.autoTags ?? true;
autoFollowUps = $settings?.autoFollowUps ?? true;
highContrastMode = $settings.highContrastMode ?? false;
highContrastMode = $settings?.highContrastMode ?? false;
detectArtifacts = $settings.detectArtifacts ?? true;
responseAutoCopy = $settings.responseAutoCopy ?? false;
detectArtifacts = $settings?.detectArtifacts ?? true;
responseAutoCopy = $settings?.responseAutoCopy ?? false;
showUsername = $settings.showUsername ?? false;
showUpdateToast = $settings.showUpdateToast ?? true;
showChangelog = $settings.showChangelog ?? true;
showUsername = $settings?.showUsername ?? false;
showUpdateToast = $settings?.showUpdateToast ?? true;
showChangelog = $settings?.showChangelog ?? true;
showEmojiInCall = $settings.showEmojiInCall ?? false;
voiceInterruption = $settings.voiceInterruption ?? false;
showEmojiInCall = $settings?.showEmojiInCall ?? false;
voiceInterruption = $settings?.voiceInterruption ?? false;
richTextInput = $settings.richTextInput ?? true;
promptAutocomplete = $settings.promptAutocomplete ?? false;
largeTextAsFile = $settings.largeTextAsFile ?? false;
copyFormatted = $settings.copyFormatted ?? false;
richTextInput = $settings?.richTextInput ?? true;
promptAutocomplete = $settings?.promptAutocomplete ?? false;
largeTextAsFile = $settings?.largeTextAsFile ?? false;
copyFormatted = $settings?.copyFormatted ?? false;
collapseCodeBlocks = $settings.collapseCodeBlocks ?? false;
expandDetails = $settings.expandDetails ?? false;
collapseCodeBlocks = $settings?.collapseCodeBlocks ?? false;
expandDetails = $settings?.expandDetails ?? false;
landingPageMode = $settings.landingPageMode ?? '';
chatBubble = $settings.chatBubble ?? true;
widescreenMode = $settings.widescreenMode ?? false;
splitLargeChunks = $settings.splitLargeChunks ?? false;
scrollOnBranchChange = $settings.scrollOnBranchChange ?? true;
chatDirection = $settings.chatDirection ?? 'auto';
userLocation = $settings.userLocation ?? false;
landingPageMode = $settings?.landingPageMode ?? '';
chatBubble = $settings?.chatBubble ?? true;
widescreenMode = $settings?.widescreenMode ?? false;
splitLargeChunks = $settings?.splitLargeChunks ?? false;
scrollOnBranchChange = $settings?.scrollOnBranchChange ?? true;
chatDirection = $settings?.chatDirection ?? 'auto';
userLocation = $settings?.userLocation ?? false;
notificationSound = $settings?.notificationSound ?? true;
notificationSoundAlways = $settings?.notificationSoundAlways ?? false;
@@ -331,23 +331,24 @@
stylizedPdfExport = $settings?.stylizedPdfExport ?? true;
hapticFeedback = $settings.hapticFeedback ?? false;
ctrlEnterToSend = $settings.ctrlEnterToSend ?? false;
hapticFeedback = $settings?.hapticFeedback ?? false;
ctrlEnterToSend = $settings?.ctrlEnterToSend ?? false;
imageCompression = $settings.imageCompression ?? false;
imageCompressionSize = $settings.imageCompressionSize ?? { width: '', height: '' };
imageCompression = $settings?.imageCompression ?? false;
imageCompressionSize = $settings?.imageCompressionSize ?? { width: '', height: '' };
defaultModelId = $settings?.models?.at(0) ?? '';
if ($config?.default_models) {
defaultModelId = $config.default_models.split(',')[0];
}
backgroundImageUrl = $settings.backgroundImageUrl ?? null;
webSearch = $settings.webSearch ?? null;
backgroundImageUrl = $settings?.backgroundImageUrl ?? null;
webSearch = $settings?.webSearch ?? null;
});
</script>
<form
id="tab-interface"
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
updateInterfaceHandler();
@@ -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();
File diff suppressed because it is too large Load Diff
+81 -3
View File
@@ -11,10 +11,13 @@
import { oneDark } from '@codemirror/theme-one-dark';
import { onMount, createEventDispatcher, getContext, tick } from 'svelte';
import { onMount, createEventDispatcher, getContext, tick, onDestroy } from 'svelte';
import PyodideWorker from '$lib/workers/pyodide.worker?worker';
import { formatPythonCode } from '$lib/apis/utils';
import { toast } from 'svelte-sonner';
import { user } from '$lib/stores';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
@@ -113,13 +116,82 @@
return await language?.load();
};
let pyodideWorkerInstance = null;
const getPyodideWorker = () => {
if (!pyodideWorkerInstance) {
pyodideWorkerInstance = new PyodideWorker(); // Your worker constructor
}
return pyodideWorkerInstance;
};
// Generate unique IDs for requests
let _formatReqId = 0;
const formatPythonCodePyodide = (code) => {
return new Promise((resolve, reject) => {
const id = `format-${++_formatReqId}`;
let timeout;
const worker = getPyodideWorker();
const script = `
import black
print(black.format_str("""${code.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/"/g, '\\"')}""", mode=black.Mode()))
`;
const packages = ['black'];
function handleMessage(event) {
const { id: eventId, stdout, stderr } = event.data;
if (eventId !== id) return; // Only handle our message
clearTimeout(timeout);
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
if (stderr) {
reject(stderr);
} else {
const formatted = stdout && typeof stdout === 'string' ? stdout.trim() : '';
resolve({ code: formatted });
}
}
function handleError(event) {
clearTimeout(timeout);
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
reject(event.message || 'Pyodide worker error');
}
worker.addEventListener('message', handleMessage);
worker.addEventListener('error', handleError);
// Send to worker
worker.postMessage({ id, code: script, packages });
// Timeout
timeout = setTimeout(() => {
worker.removeEventListener('message', handleMessage);
worker.removeEventListener('error', handleError);
try {
worker.terminate();
} catch {}
pyodideWorkerInstance = null;
reject('Execution Time Limit Exceeded');
}, 60000);
});
};
export const formatPythonCodeHandler = async () => {
if (codeEditor) {
const res = await formatPythonCode(localStorage.token, _value).catch((error) => {
const res = await (
$user?.role === 'admin'
? formatPythonCode(localStorage.token, _value)
: formatPythonCodePyodide(_value)
).catch((error) => {
toast.error(`${error}`);
return null;
});
if (res && res.code) {
const formattedCode = res.code;
codeEditor.dispatch({
@@ -240,6 +312,12 @@
document.removeEventListener('keydown', keydownHandler);
};
});
onDestroy(() => {
if (pyodideWorkerInstance) {
pyodideWorkerInstance.terminate();
}
});
</script>
<div id="code-textarea-{id}" class="h-full w-full text-sm" />
@@ -13,6 +13,9 @@
dayjs.extend(relativeTime);
async function loadLocale(locales) {
if (!locales || !Array.isArray(locales)) {
return;
}
for (const locale of locales) {
try {
dayjs.locale(locale);
+1 -3
View File
@@ -2,10 +2,8 @@
import DOMPurify from 'dompurify';
import { onDestroy } from 'svelte';
import { marked } from 'marked';
import tippy from 'tippy.js';
import { roundArrow } from 'tippy.js';
export let placement = 'top';
export let content = `I'm a tooltip!`;
@@ -47,6 +45,6 @@
});
</script>
<div bind:this={tooltipElement} aria-label={DOMPurify.sanitize(content)} class={className}>
<div bind:this={tooltipElement} class={className}>
<slot />
</div>
+1
View File
@@ -7,6 +7,7 @@
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
+46 -3
View File
@@ -21,7 +21,8 @@
channels,
socket,
config,
isApp
isApp,
models
} from '$lib/stores';
import { onMount, getContext, tick, onDestroy } from 'svelte';
@@ -489,6 +490,8 @@
draggable="false"
on:click={async () => {
selectedChatId = null;
await temporaryChatEnabled.set(false);
await goto('/');
const newChatButton = document.getElementById('new-chat-button');
setTimeout(() => {
@@ -649,6 +652,46 @@
? 'opacity-20'
: ''}"
>
{#if ($models ?? []).length > 0 && ($settings?.pinnedModels ?? []).length > 0}
<div class="mt-0.5">
{#each $settings.pinnedModels as modelId (modelId)}
{@const model = $models.find((model) => model.id === modelId)}
{#if model}
<div class="px-1.5 flex justify-center text-gray-800 dark:text-gray-200">
<a
class="grow flex items-center space-x-2.5 rounded-lg px-2 py-[7px] hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/?model={modelId}"
on:click={() => {
selectedChatId = null;
chatId.set('');
if ($mobile) {
showSidebar.set(false);
}
}}
draggable="false"
>
<div class="self-center shrink-0">
<img
crossorigin="anonymous"
src={model?.info?.meta?.profile_image_url ?? '/static/favicon.png'}
class=" size-5 rounded-full -translate-x-[0.5px]"
alt="logo"
/>
</div>
<div class="flex self-center translate-y-[0.5px]">
<div class=" self-center font-medium text-sm font-primary line-clamp-1">
{model?.name ?? modelId}
</div>
</div>
</a>
</div>
{/if}
{/each}
</div>
{/if}
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || $channels.length > 0)}
<Folder
className="px-2 mt-0.5"
@@ -785,7 +828,7 @@
<div
class="ml-3 pl-1 mt-[1px] flex flex-col overflow-y-auto scrollbar-hidden border-s border-gray-100 dark:border-gray-900"
>
{#each $pinnedChats as chat, idx}
{#each $pinnedChats as chat, idx (`pinned-chat-${chat?.id ?? idx}`)}
<ChatItem
className=""
id={chat.id}
@@ -831,7 +874,7 @@
<div class=" flex-1 flex flex-col overflow-y-auto scrollbar-hidden">
<div class="pt-1.5">
{#if $chats}
{#each $chats as chat, idx}
{#each $chats as chat, idx (`chat-${chat?.id ?? idx}`)}
{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
<div
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx ===
@@ -204,9 +204,10 @@
const chatTitleInputKeydownHandler = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
editChatTitle(id, chatTitle);
confirmEdit = false;
chatTitle = '';
setTimeout(() => {
const input = document.getElementById(`chat-title-input-${id}`);
if (input) input.blur();
}, 0);
} else if (e.key === 'Escape') {
e.preventDefault();
confirmEdit = false;
@@ -29,7 +29,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-lg px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
class="w-full max-w-[170px] rounded-lg px-1 py-1.5 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-lg"
sideOffset={-2}
side="bottom"
align="start"
+1 -1
View File
@@ -13,7 +13,7 @@
const i18n = getContext('i18n');
export let show = false;
export let className = 'max-w-[160px]';
export let className = 'max-w-[170px]';
export let onRecord = () => {};
export let onCaptureAudio = () => {};
@@ -52,7 +52,7 @@
<div class=" pt-1">
<button
class=" group-hover:text-gray-500 dark:text-gray-900 dark:hover:text-gray-300 transition"
class=" group-hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-300 transition"
on:click={() => {
onDelete();
}}
@@ -49,7 +49,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="end"
@@ -48,7 +48,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"
+10 -2
View File
@@ -22,7 +22,7 @@
import ChevronRight from '../icons/ChevronRight.svelte';
import Spinner from '../common/Spinner.svelte';
import Tooltip from '../common/Tooltip.svelte';
import { capitalizeFirstLetter } from '$lib/utils';
import { capitalizeFirstLetter, slugify } from '$lib/utils';
import XMark from '../icons/XMark.svelte';
const i18n = getContext('i18n');
@@ -68,7 +68,15 @@
};
const cloneHandler = async (prompt) => {
sessionStorage.prompt = JSON.stringify(prompt);
const clonedPrompt = { ...prompt };
clonedPrompt.title = `${clonedPrompt.title} (Clone)`;
const baseCommand = clonedPrompt.command.startsWith('/')
? clonedPrompt.command.substring(1)
: clonedPrompt.command;
clonedPrompt.command = slugify(`${baseCommand} clone`);
sessionStorage.prompt = JSON.stringify(clonedPrompt);
goto('/workspace/prompts/create');
};
@@ -13,6 +13,7 @@
export let onSubmit: Function;
export let edit = false;
export let prompt = null;
export let clone = false;
const i18n = getContext('i18n');
@@ -39,7 +39,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"
@@ -40,7 +40,7 @@
<div slot="content">
<DropdownMenu.Content
class="w-full max-w-[160px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
class="w-full max-w-[170px] rounded-xl px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-850 dark:text-white shadow-sm"
sideOffset={-2}
side="bottom"
align="start"