Merge branch 'dev' into leaderboard-evaluation-additions
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('&', '&')}
|
||||
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('&', '&')}
|
||||
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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user