Unverified Commit 85175470 authored by Timothy Jaeryang Baek's avatar Timothy Jaeryang Baek Committed by GitHub
Browse files

Merge pull request #1555 from open-webui/dev

0.1.119
parents 0399a69b 375056f8
<script>
import {
addTagById,
deleteTagById,
getAllChatTags,
getTagsById,
updateChatById
} from '$lib/apis/chats';
import { tags as _tags } from '$lib/stores';
import { onMount } from 'svelte';
import Tags from '../common/Tags.svelte';
export let chatId = '';
let tags = [];
const getTags = async () => {
return await getTagsById(localStorage.token, chatId).catch(async (error) => {
return [];
});
};
const addTag = async (tagName) => {
const res = await addTagById(localStorage.token, chatId, tagName);
tags = await getTags();
await updateChatById(localStorage.token, chatId, {
tags: tags
});
_tags.set(await getAllChatTags(localStorage.token));
};
const deleteTag = async (tagName) => {
const res = await deleteTagById(localStorage.token, chatId, tagName);
tags = await getTags();
await updateChatById(localStorage.token, chatId, {
tags: tags
});
_tags.set(await getAllChatTags(localStorage.token));
};
onMount(async () => {
if (chatId) {
tags = await getTags();
}
});
</script>
<Tags {tags} {deleteTag} {addTag} />
...@@ -4,10 +4,12 @@ ...@@ -4,10 +4,12 @@
import { flyAndScale } from '$lib/utils/transitions'; import { flyAndScale } from '$lib/utils/transitions';
export let show = false;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
</script> </script>
<DropdownMenu.Root <DropdownMenu.Root
bind:open={show}
onOpenChange={(state) => { onOpenChange={(state) => {
dispatch('change', state); dispatch('change', state);
}} }}
......
<script lang="ts">
import { Pagination } from 'bits-ui';
import { createEventDispatcher } from 'svelte';
import ChevronLeft from '../icons/ChevronLeft.svelte';
import ChevronRight from '../icons/ChevronRight.svelte';
export let page = 0;
export let count = 0;
export let perPage = 20;
</script>
<div class="flex justify-center">
<Pagination.Root bind:page {count} {perPage} let:pages>
<div class="my-2 flex items-center">
<Pagination.PrevButton
class="mr-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent"
>
<ChevronLeft className="size-4" strokeWidth="2" />
</Pagination.PrevButton>
<div class="flex items-center gap-2.5">
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<div class="text-sm font-medium text-foreground-alt">...</div>
{:else}
<Pagination.Page
{page}
class="inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 text-sm font-medium hover:bg-dark-10 active:scale-98 disabled:cursor-not-allowed disabled:opacity-50 hover:disabled:bg-transparent data-[selected]:bg-black data-[selected]:text-gray-100 data-[selected]:hover:bg-black dark:data-[selected]:bg-white dark:data-[selected]:text-gray-900 dark:data-[selected]:hover:bg-white"
>
{page.value}
</Pagination.Page>
{/if}
{/each}
</div>
<Pagination.NextButton
class="ml-[25px] inline-flex size-8 items-center justify-center rounded-[9px] bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 active:scale-98 disabled:cursor-not-allowed disabled:text-gray-400 dark:disabled:text-gray-700 hover:disabled:bg-transparent dark:hover:disabled:bg-transparent"
>
<ChevronRight className="size-4" strokeWidth="2" />
</Pagination.NextButton>
</div>
</Pagination.Root>
</div>
...@@ -16,7 +16,6 @@ ...@@ -16,7 +16,6 @@
const i18n = getContext('i18n'); const i18n = getContext('i18n');
export let show = false; export let show = false;
export let selectedDoc;
let uploadDocInputElement: HTMLInputElement; let uploadDocInputElement: HTMLInputElement;
let inputFiles; let inputFiles;
let tags = []; let tags = [];
......
...@@ -7,11 +7,11 @@ ...@@ -7,11 +7,11 @@
scanDocs, scanDocs,
updateQuerySettings, updateQuerySettings,
resetVectorDB, resetVectorDB,
getEmbeddingModel, getEmbeddingConfig,
updateEmbeddingModel updateEmbeddingConfig
} from '$lib/apis/rag'; } from '$lib/apis/rag';
import { documents } from '$lib/stores'; import { documents, models } from '$lib/stores';
import { onMount, getContext } from 'svelte'; import { onMount, getContext } from 'svelte';
import { toast } from 'svelte-sonner'; import { toast } from 'svelte-sonner';
...@@ -26,6 +26,12 @@ ...@@ -26,6 +26,12 @@
let showResetConfirm = false; let showResetConfirm = false;
let embeddingEngine = '';
let embeddingModel = '';
let openAIKey = '';
let openAIUrl = '';
let chunkSize = 0; let chunkSize = 0;
let chunkOverlap = 0; let chunkOverlap = 0;
let pdfExtractImages = true; let pdfExtractImages = true;
...@@ -35,8 +41,6 @@ ...@@ -35,8 +41,6 @@
k: 4 k: 4
}; };
let embeddingModel = '';
const scanHandler = async () => { const scanHandler = async () => {
scanDirLoading = true; scanDirLoading = true;
const res = await scanDocs(localStorage.token); const res = await scanDocs(localStorage.token);
...@@ -49,7 +53,15 @@ ...@@ -49,7 +53,15 @@
}; };
const embeddingModelUpdateHandler = async () => { const embeddingModelUpdateHandler = async () => {
if (embeddingModel.split('/').length - 1 > 1) { if (embeddingEngine === '' && embeddingModel.split('/').length - 1 > 1) {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
)
);
return;
}
if (embeddingEngine === 'ollama' && embeddingModel === '') {
toast.error( toast.error(
$i18n.t( $i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.' 'Model filesystem path detected. Model shortname is required for update, cannot continue.'
...@@ -58,14 +70,37 @@ ...@@ -58,14 +70,37 @@
return; return;
} }
if (embeddingEngine === 'openai' && embeddingModel === '') {
toast.error(
$i18n.t(
'Model filesystem path detected. Model shortname is required for update, cannot continue.'
)
);
return;
}
if ((embeddingEngine === 'openai' && openAIKey === '') || openAIUrl === '') {
toast.error($i18n.t('OpenAI URL/Key required.'));
return;
}
console.log('Update embedding model attempt:', embeddingModel); console.log('Update embedding model attempt:', embeddingModel);
updateEmbeddingModelLoading = true; updateEmbeddingModelLoading = true;
const res = await updateEmbeddingModel(localStorage.token, { const res = await updateEmbeddingConfig(localStorage.token, {
embedding_model: embeddingModel embedding_engine: embeddingEngine,
embedding_model: embeddingModel,
...(embeddingEngine === 'openai'
? {
openai_config: {
key: openAIKey,
url: openAIUrl
}
}
: {})
}).catch(async (error) => { }).catch(async (error) => {
toast.error(error); toast.error(error);
embeddingModel = (await getEmbeddingModel(localStorage.token)).embedding_model; await setEmbeddingConfig();
return null; return null;
}); });
updateEmbeddingModelLoading = false; updateEmbeddingModelLoading = false;
...@@ -73,7 +108,7 @@ ...@@ -73,7 +108,7 @@
if (res) { if (res) {
console.log('embeddingModelUpdateHandler:', res); console.log('embeddingModelUpdateHandler:', res);
if (res.status === true) { if (res.status === true) {
toast.success($i18n.t('Model {{embedding_model}} update complete!', res), { toast.success($i18n.t('Embedding model set to "{{embedding_model}}"', res), {
duration: 1000 * 10 duration: 1000 * 10
}); });
} }
...@@ -91,6 +126,18 @@ ...@@ -91,6 +126,18 @@
querySettings = await updateQuerySettings(localStorage.token, querySettings); querySettings = await updateQuerySettings(localStorage.token, querySettings);
}; };
const setEmbeddingConfig = async () => {
const embeddingConfig = await getEmbeddingConfig(localStorage.token);
if (embeddingConfig) {
embeddingEngine = embeddingConfig.embedding_engine;
embeddingModel = embeddingConfig.embedding_model;
openAIKey = embeddingConfig.openai_config.key;
openAIUrl = embeddingConfig.openai_config.url;
}
};
onMount(async () => { onMount(async () => {
const res = await getRAGConfig(localStorage.token); const res = await getRAGConfig(localStorage.token);
...@@ -101,7 +148,7 @@ ...@@ -101,7 +148,7 @@
chunkOverlap = res.chunk.chunk_overlap; chunkOverlap = res.chunk.chunk_overlap;
} }
embeddingModel = (await getEmbeddingModel(localStorage.token)).embedding_model; await setEmbeddingConfig();
querySettings = await getQuerySettings(localStorage.token); querySettings = await getQuerySettings(localStorage.token);
}); });
...@@ -119,27 +166,80 @@ ...@@ -119,27 +166,80 @@
<div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div> <div class=" mb-2 text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" flex w-full justify-between"> <div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium"> <div class=" self-center text-xs font-medium">{$i18n.t('Embedding Model Engine')}</div>
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })} <div class="flex items-center relative">
<select
class="dark:bg-gray-900 w-fit pr-8 rounded px-2 p-1 text-xs bg-transparent outline-none text-right"
bind:value={embeddingEngine}
placeholder="Select an embedding model engine"
on:change={(e) => {
if (e.target.value === 'ollama') {
embeddingModel = '';
} else if (e.target.value === 'openai') {
embeddingModel = 'text-embedding-3-small';
}
}}
>
<option value="">{$i18n.t('Default (SentenceTransformer)')}</option>
<option value="ollama">{$i18n.t('Ollama')}</option>
<option value="openai">{$i18n.t('OpenAI')}</option>
</select>
</div>
</div>
{#if embeddingEngine === 'openai'}
<div class="mt-1 flex gap-2">
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Base URL')}
bind:value={openAIUrl}
required
/>
<input
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
placeholder={$i18n.t('API Key')}
bind:value={openAIKey}
required
/>
</div>
{/if}
</div> </div>
<div class="space-y-2">
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Embedding Model')}</div>
{#if embeddingEngine === 'ollama'}
<div class="flex w-full">
<div class="flex-1 mr-2">
<select
class="w-full rounded-lg py-2 px-4 text-sm dark:text-gray-300 dark:bg-gray-850 outline-none"
bind:value={embeddingModel}
placeholder={$i18n.t('Select a model')}
required
>
{#if !embeddingModel}
<option value="" disabled selected>{$i18n.t('Select a model')}</option>
{/if}
{#each $models.filter((m) => m.id && !m.external) as model}
<option value={model.name} class="bg-gray-100 dark:bg-gray-700"
>{model.name + ' (' + (model.size / 1024 ** 3).toFixed(1) + ' GB)'}</option
>
{/each}
</select>
</div>
<button <button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading class="px-2.5 bg-gray-100 hover:bg-gray-200 text-gray-800 dark:bg-gray-850 dark:hover:bg-gray-800 dark:text-gray-100 rounded-lg transition"
? ' cursor-not-allowed'
: ''}"
on:click={() => { on:click={() => {
scanHandler(); embeddingModelUpdateHandler();
console.log('check');
}} }}
type="button" disabled={updateEmbeddingModelLoading}
disabled={scanDirLoading}
> >
<div class="self-center font-medium">{$i18n.t('Scan')}</div> {#if updateEmbeddingModelLoading}
<div class="self-center">
{#if scanDirLoading}
<div class="ml-3 self-center">
<svg <svg
class=" w-3 h-3" class=" w-4 h-4"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="currentColor" fill="currentColor"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
...@@ -162,16 +262,23 @@ ...@@ -162,16 +262,23 @@
/></svg /></svg
> >
</div> </div>
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M12.416 3.376a.75.75 0 0 1 .208 1.04l-5 7.5a.75.75 0 0 1-1.154.114l-3-3a.75.75 0 0 1 1.06-1.06l2.353 2.353 4.493-6.74a.75.75 0 0 1 1.04-.207Z"
clip-rule="evenodd"
/>
</svg>
{/if} {/if}
</button> </button>
</div> </div>
</div> {:else}
<hr class=" dark:border-gray-700" />
<div class="space-y-2">
<div>
<div class=" mb-2 text-sm font-medium">{$i18n.t('Update Embedding Model')}</div>
<div class="flex w-full"> <div class="flex w-full">
<div class="flex-1 mr-2"> <div class="flex-1 mr-2">
<input <input
...@@ -232,6 +339,7 @@ ...@@ -232,6 +339,7 @@
{/if} {/if}
</button> </button>
</div> </div>
{/if}
<div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500"> <div class="mt-2 mb-1 text-xs text-gray-400 dark:text-gray-500">
{$i18n.t( {$i18n.t(
...@@ -241,6 +349,56 @@ ...@@ -241,6 +349,56 @@
<hr class=" dark:border-gray-700 my-3" /> <hr class=" dark:border-gray-700 my-3" />
<div class=" flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Scan for documents from {{path}}', { path: '/data/docs' })}
</div>
<button
class=" self-center text-xs p-1 px-3 bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 rounded-lg flex flex-row space-x-1 items-center {scanDirLoading
? ' cursor-not-allowed'
: ''}"
on:click={() => {
scanHandler();
console.log('check');
}}
type="button"
disabled={scanDirLoading}
>
<div class="self-center font-medium">{$i18n.t('Scan')}</div>
{#if scanDirLoading}
<div class="ml-3 self-center">
<svg
class=" w-3 h-3"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
<hr class=" dark:border-gray-700 my-3" />
<div class=" "> <div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div> <div class=" text-sm font-medium">{$i18n.t('Chunk Params')}</div>
......
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
</svg>
<script lang="ts">
export let className = 'w-4 h-4';
export let strokeWidth = '1.5';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
...@@ -19,10 +19,6 @@ ...@@ -19,10 +19,6 @@
export let chat; export let chat;
export let selectedModels; export let selectedModels;
export let tags = [];
export let addTag: Function;
export let deleteTag: Function;
export let showModelSelector = true; export let showModelSelector = true;
let showShareChatModal = false; let showShareChatModal = false;
...@@ -85,9 +81,6 @@ ...@@ -85,9 +81,6 @@
downloadHandler={() => { downloadHandler={() => {
showDownloadChatModal = !showDownloadChatModal; showDownloadChatModal = !showDownloadChatModal;
}} }}
{tags}
{deleteTag}
{addTag}
> >
<button <button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition" class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
......
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
import { flyAndScale } from '$lib/utils/transitions'; import { flyAndScale } from '$lib/utils/transitions';
import Dropdown from '$lib/components/common/Dropdown.svelte'; import Dropdown from '$lib/components/common/Dropdown.svelte';
import Tags from '$lib/components/common/Tags.svelte'; import Tags from '$lib/components/chat/Tags.svelte';
import { WEBUI_BASE_URL } from '$lib/constants';
import { downloadChatAsPDF } from '$lib/apis/utils'; import { downloadChatAsPDF } from '$lib/apis/utils';
export let shareEnabled: boolean = false; export let shareEnabled: boolean = false;
...@@ -21,10 +21,6 @@ ...@@ -21,10 +21,6 @@
// export let tagHandler: Function; // export let tagHandler: Function;
export let chat; export let chat;
export let tags;
export let deleteTag: Function;
export let addTag: Function;
export let onClose: Function = () => {}; export let onClose: Function = () => {};
const downloadTxt = async () => { const downloadTxt = async () => {
...@@ -190,7 +186,7 @@ ...@@ -190,7 +186,7 @@
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" /> <hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1"> <div class="flex p-1">
<Tags {tags} {deleteTag} {addTag} /> <Tags chatId={chat.id} />
</div> </div>
<!-- <DropdownMenu.Item <!-- <DropdownMenu.Item
......
...@@ -45,6 +45,39 @@ ...@@ -45,6 +45,39 @@
show = true; show = true;
} }
await chats.set(await getChatList(localStorage.token)); await chats.set(await getChatList(localStorage.token));
let touchstartX = 0;
let touchendX = 0;
function checkDirection() {
const screenWidth = window.innerWidth;
const swipeDistance = Math.abs(touchendX - touchstartX);
if (swipeDistance >= screenWidth / 4) {
if (touchendX < touchstartX) {
show = false;
}
if (touchendX > touchstartX) {
show = true;
}
}
}
const onTouchStart = (e) => {
touchstartX = e.changedTouches[0].screenX;
};
const onTouchEnd = (e) => {
touchendX = e.changedTouches[0].screenX;
checkDirection();
};
document.addEventListener('touchstart', onTouchStart);
document.addEventListener('touchend', onTouchEnd);
return () => {
document.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('touchend', onTouchEnd);
};
}); });
// Helper function to fetch and add chat content to each chat // Helper function to fetch and add chat content to each chat
...@@ -513,6 +546,7 @@ ...@@ -513,6 +546,7 @@
{:else} {:else}
<div class="flex self-center space-x-1.5 z-10"> <div class="flex self-center space-x-1.5 z-10">
<ChatMenu <ChatMenu
chatId={chat.id}
renameHandler={() => { renameHandler={() => {
chatTitle = chat.title; chatTitle = chat.title;
chatTitleEditId = chat.id; chatTitleEditId = chat.id;
...@@ -706,6 +740,7 @@ ...@@ -706,6 +740,7 @@
</div> </div>
<div <div
id="sidebar-handle"
class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0" class="fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
> >
<Tooltip <Tooltip
......
...@@ -6,14 +6,19 @@ ...@@ -6,14 +6,19 @@
import GarbageBin from '$lib/components/icons/GarbageBin.svelte'; import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
import Pencil from '$lib/components/icons/Pencil.svelte'; import Pencil from '$lib/components/icons/Pencil.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
export let renameHandler: Function; export let renameHandler: Function;
export let deleteHandler: Function; export let deleteHandler: Function;
export let onClose: Function; export let onClose: Function;
export let chatId = '';
let show = false;
</script> </script>
<Dropdown <Dropdown
bind:show
on:change={(e) => { on:change={(e) => {
if (e.detail === false) { if (e.detail === false) {
onClose(); onClose();
...@@ -26,14 +31,14 @@ ...@@ -26,14 +31,14 @@
<div slot="content"> <div slot="content">
<DropdownMenu.Content <DropdownMenu.Content
class="w-full max-w-[130px] rounded-lg 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" class="w-full max-w-[150px] rounded-lg px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-900 dark:text-white shadow"
sideOffset={-2} sideOffset={-2}
side="bottom" side="bottom"
align="start" align="start"
transition={flyAndScale} transition={flyAndScale}
> >
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => { on:click={() => {
renameHandler(); renameHandler();
}} }}
...@@ -43,7 +48,7 @@ ...@@ -43,7 +48,7 @@
</DropdownMenu.Item> </DropdownMenu.Item>
<DropdownMenu.Item <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer" class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => { on:click={() => {
deleteHandler(); deleteHandler();
}} }}
...@@ -51,6 +56,12 @@ ...@@ -51,6 +56,12 @@
<GarbageBin strokeWidth="2" /> <GarbageBin strokeWidth="2" />
<div class="flex items-center">Delete</div> <div class="flex items-center">Delete</div>
</DropdownMenu.Item> </DropdownMenu.Item>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags {chatId} />
</div>
</DropdownMenu.Content> </DropdownMenu.Content>
</div> </div>
</Dropdown> </Dropdown>
...@@ -63,7 +63,7 @@ ...@@ -63,7 +63,7 @@
"Click here to select": "Presiona aquí para seleccionar", "Click here to select": "Presiona aquí para seleccionar",
"Click here to select documents.": "Presiona aquí para seleccionar documentos", "Click here to select documents.": "Presiona aquí para seleccionar documentos",
"click here.": "Presiona aquí.", "click here.": "Presiona aquí.",
"Click on the user role button to change a user's role.": "Presiona en el botón de roles del usuario para cambiar el rol de un usuario.", "Click on the user role button to change a user's role.": "Presiona en el botón de roles del usuario para cambiar su rol.",
"Close": "Cerrar", "Close": "Cerrar",
"Collection": "Colección", "Collection": "Colección",
"Command": "Comando", "Command": "Comando",
...@@ -120,9 +120,10 @@ ...@@ -120,9 +120,10 @@
"Edit Doc": "Editar Documento", "Edit Doc": "Editar Documento",
"Edit User": "Editar Usuario", "Edit User": "Editar Usuario",
"Email": "Email", "Email": "Email",
"Embedding model: {{embedding_model}}": "Modelo de Embedding: {{embedding_model}}",
"Enable Chat History": "Activa el Historial de Chat", "Enable Chat History": "Activa el Historial de Chat",
"Enable New Sign Ups": "Habilitar Nuevos Registros", "Enable New Sign Ups": "Habilitar Nuevos Registros",
"Enabled": "Habilitado", "Enabled": "Activado",
"Enter {{role}} message here": "Introduzca el mensaje {{role}} aquí", "Enter {{role}} message here": "Introduzca el mensaje {{role}} aquí",
"Enter API Key": "Ingrese la clave API", "Enter API Key": "Ingrese la clave API",
"Enter Chunk Overlap": "Ingresar superposición de fragmentos", "Enter Chunk Overlap": "Ingresar superposición de fragmentos",
...@@ -145,11 +146,12 @@ ...@@ -145,11 +146,12 @@
"Export All Chats (All Users)": "Exportar todos los chats (Todos los usuarios)", "Export All Chats (All Users)": "Exportar todos los chats (Todos los usuarios)",
"Export Chats": "Exportar Chats", "Export Chats": "Exportar Chats",
"Export Documents Mapping": "Exportar el mapeo de documentos", "Export Documents Mapping": "Exportar el mapeo de documentos",
"Export Modelfiles": "Exportal Modelfiles", "Export Modelfiles": "Exportar Modelfiles",
"Export Prompts": "Exportar Prompts", "Export Prompts": "Exportar Prompts",
"Failed to read clipboard contents": "No se pudo leer el contenido del portapapeles", "Failed to read clipboard contents": "No se pudo leer el contenido del portapapeles",
"File Mode": "Modo de archivo", "File Mode": "Modo de archivo",
"File not found.": "Archivo no encontrado.", "File not found.": "Archivo no encontrado.",
"Fingerprint spoofing detected: Unable to use initials as avatar. Defaulting to default profile image.": "Se detectó suplantación de huellas: No se pueden usar las iniciales como avatar. Por defecto se utiliza la imagen de perfil predeterminada.",
"Focus chat input": "Enfoca la entrada del chat", "Focus chat input": "Enfoca la entrada del chat",
"Format your variables using square brackets like this:": "Formatee sus variables usando corchetes así:", "Format your variables using square brackets like this:": "Formatee sus variables usando corchetes así:",
"From (Base Model)": "Desde (Modelo Base)", "From (Base Model)": "Desde (Modelo Base)",
...@@ -193,8 +195,11 @@ ...@@ -193,8 +195,11 @@
"MMMM DD, YYYY": "MMMM DD, YYYY", "MMMM DD, YYYY": "MMMM DD, YYYY",
"Model '{{modelName}}' has been successfully downloaded.": "El modelo '{{modelName}}' se ha descargado correctamente.", "Model '{{modelName}}' has been successfully downloaded.": "El modelo '{{modelName}}' se ha descargado correctamente.",
"Model '{{modelTag}}' is already in queue for downloading.": "El modelo '{{modelTag}}' ya está en cola para descargar.", "Model '{{modelTag}}' is already in queue for downloading.": "El modelo '{{modelTag}}' ya está en cola para descargar.",
"Model {{embedding_model}} update complete!": "¡La actualizacón del modelo {{embedding_model}} fué completada!",
"Model {{embedding_model}} update failed or not required!": "¡La actualización del modelo {{embedding_model}} falló o no es requerida!",
"Model {{modelId}} not found": "El modelo {{modelId}} no fue encontrado", "Model {{modelId}} not found": "El modelo {{modelId}} no fue encontrado",
"Model {{modelName}} already exists.": "El modelo {{modelName}} ya existe.", "Model {{modelName}} already exists.": "El modelo {{modelName}} ya existe.",
"Model filesystem path detected. Model shortname is required for update, cannot continue.": "Se detectó la ruta del sistema de archivos del modelo. Se requiere el nombre corto del modelo para la actualización, no se puede continuar.",
"Model Name": "Nombre del modelo", "Model Name": "Nombre del modelo",
"Model not selected": "Modelo no seleccionado", "Model not selected": "Modelo no seleccionado",
"Model Tag Name": "Nombre de la etiqueta del modelo", "Model Tag Name": "Nombre de la etiqueta del modelo",
...@@ -215,11 +220,11 @@ ...@@ -215,11 +220,11 @@
"New Password": "Nueva Contraseña", "New Password": "Nueva Contraseña",
"Not sure what to add?": "¿No estás seguro de qué añadir?", "Not sure what to add?": "¿No estás seguro de qué añadir?",
"Not sure what to write? Switch to": "¿No estás seguro de qué escribir? Cambia a", "Not sure what to write? Switch to": "¿No estás seguro de qué escribir? Cambia a",
"Off": "Apagado", "Off": "Desactivado",
"Okay, Let's Go!": "Okay, Let's Go!", "Okay, Let's Go!": "Okay, Let's Go!",
"Ollama Base URL": "URL base de Ollama", "Ollama Base URL": "URL base de Ollama",
"Ollama Version": "Version de Ollama", "Ollama Version": "Version de Ollama",
"On": "Encendido", "On": "Activado",
"Only": "Solamente", "Only": "Solamente",
"Only alphanumeric characters and hyphens are allowed in the command string.": "Sólo se permiten caracteres alfanuméricos y guiones en la cadena de comando.", "Only alphanumeric characters and hyphens are allowed in the command string.": "Sólo se permiten caracteres alfanuméricos y guiones en la cadena de comando.",
"Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "¡Ups! ¡Agárrate fuerte! Tus archivos todavía están en el horno de procesamiento. Los estamos cocinando a la perfección. Tenga paciencia y le avisaremos una vez que estén listos.", "Oops! Hold tight! Your files are still in the processing oven. We're cooking them up to perfection. Please be patient and we'll let you know once they're ready.": "¡Ups! ¡Agárrate fuerte! Tus archivos todavía están en el horno de procesamiento. Los estamos cocinando a la perfección. Tenga paciencia y le avisaremos una vez que estén listos.",
...@@ -227,7 +232,7 @@ ...@@ -227,7 +232,7 @@
"Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "¡Ups! Estás utilizando un método no compatible (solo frontend). Sirve la WebUI desde el backend.", "Oops! You're using an unsupported method (frontend only). Please serve the WebUI from the backend.": "¡Ups! Estás utilizando un método no compatible (solo frontend). Sirve la WebUI desde el backend.",
"Open": "Abrir", "Open": "Abrir",
"Open AI": "Open AI", "Open AI": "Open AI",
"Open AI (Dall-E)": "", "Open AI (Dall-E)": "Open AI (Dall-E)",
"Open new chat": "Abrir nuevo chat", "Open new chat": "Abrir nuevo chat",
"OpenAI API": "OpenAI API", "OpenAI API": "OpenAI API",
"OpenAI API Key": "Clave de OpenAI API", "OpenAI API Key": "Clave de OpenAI API",
...@@ -243,7 +248,7 @@ ...@@ -243,7 +248,7 @@
"Prompt Content": "Contenido del Prompt", "Prompt Content": "Contenido del Prompt",
"Prompt suggestions": "Sugerencias de Prompts", "Prompt suggestions": "Sugerencias de Prompts",
"Prompts": "Prompts", "Prompts": "Prompts",
"Pull a model from Ollama.com": "Extraer un modelo de Ollama.com", "Pull a model from Ollama.com": "Halar un modelo de Ollama.com",
"Pull Progress": "Progreso de extracción", "Pull Progress": "Progreso de extracción",
"Query Params": "Parámetros de consulta", "Query Params": "Parámetros de consulta",
"RAG Template": "Plantilla de RAG", "RAG Template": "Plantilla de RAG",
...@@ -256,7 +261,7 @@ ...@@ -256,7 +261,7 @@
"Request Mode": "Modo de petición", "Request Mode": "Modo de petición",
"Reset Vector Storage": "Restablecer almacenamiento vectorial", "Reset Vector Storage": "Restablecer almacenamiento vectorial",
"Response AutoCopy to Clipboard": "Copiar respuesta automáticamente al portapapeles", "Response AutoCopy to Clipboard": "Copiar respuesta automáticamente al portapapeles",
"Role": "personalizados", "Role": "Rol",
"Rosé Pine": "Rosé Pine", "Rosé Pine": "Rosé Pine",
"Rosé Pine Dawn": "Rosé Pine Dawn", "Rosé Pine Dawn": "Rosé Pine Dawn",
"Save": "Guardar", "Save": "Guardar",
...@@ -332,7 +337,10 @@ ...@@ -332,7 +337,10 @@
"TTS Settings": "Configuración de TTS", "TTS Settings": "Configuración de TTS",
"Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL", "Type Hugging Face Resolve (Download) URL": "Type Hugging Face Resolve (Download) URL",
"Uh-oh! There was an issue connecting to {{provider}}.": "¡UH oh! Hubo un problema al conectarse a {{provider}}.", "Uh-oh! There was an issue connecting to {{provider}}.": "¡UH oh! Hubo un problema al conectarse a {{provider}}.",
"Understand that updating or changing your embedding model requires reset of the vector database and re-import of all documents. You have been warned!": "Comprenda que actualizar o cambiar su modelo de embedding requiere restablecer la base de datos de vectores y volver a importar todos los documentos. ¡Usted ha sido advertido!",
"Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo de archivo desconocido '{{file_type}}', pero se acepta y se trata como texto sin formato", "Unknown File Type '{{file_type}}', but accepting and treating as plain text": "Tipo de archivo desconocido '{{file_type}}', pero se acepta y se trata como texto sin formato",
"Update": "Actualizar",
"Update embedding model {{embedding_model}}": "Actualizar modelo de embedding {{embedding_model}}",
"Update password": "Actualiza contraseña", "Update password": "Actualiza contraseña",
"Upload a GGUF model": "Sube un modelo GGUF", "Upload a GGUF model": "Sube un modelo GGUF",
"Upload files": "Subir archivos", "Upload files": "Subir archivos",
...@@ -340,6 +348,7 @@ ...@@ -340,6 +348,7 @@
"URL Mode": "Modo de URL", "URL Mode": "Modo de URL",
"Use '#' in the prompt input to load and select your documents.": "Utilice '#' en el prompt para cargar y seleccionar sus documentos.", "Use '#' in the prompt input to load and select your documents.": "Utilice '#' en el prompt para cargar y seleccionar sus documentos.",
"Use Gravatar": "Usar Gravatar", "Use Gravatar": "Usar Gravatar",
"Use Initials": "Usar Iniciales",
"user": "usuario", "user": "usuario",
"User Permissions": "Permisos de usuario", "User Permissions": "Permisos de usuario",
"Users": "Usuarios", "Users": "Usuarios",
...@@ -347,7 +356,7 @@ ...@@ -347,7 +356,7 @@
"Valid time units:": "Unidades válidas de tiempo:", "Valid time units:": "Unidades válidas de tiempo:",
"variable": "variable", "variable": "variable",
"variable to have them replaced with clipboard content.": "variable para reemplazarlos con el contenido del portapapeles.", "variable to have them replaced with clipboard content.": "variable para reemplazarlos con el contenido del portapapeles.",
"Version": "Version", "Version": "Versión",
"Web": "Web", "Web": "Web",
"WebUI Add-ons": "WebUI Add-ons", "WebUI Add-ons": "WebUI Add-ons",
"WebUI Settings": "Configuración del WebUI", "WebUI Settings": "Configuración del WebUI",
......
...@@ -52,13 +52,17 @@ ...@@ -52,13 +52,17 @@
"title": "Dutch (Netherlands)" "title": "Dutch (Netherlands)"
}, },
{ {
"code": "pt-PT", "code": "pl-PL",
"title": "Portuguese (Portugal)" "title": "Polish"
}, },
{ {
"code": "pt-BR", "code": "pt-BR",
"title": "Portuguese (Brazil)" "title": "Portuguese (Brazil)"
}, },
{
"code": "pt-PT",
"title": "Portuguese (Portugal)"
},
{ {
"code": "ru-RU", "code": "ru-RU",
"title": "Russian (Russia)" "title": "Russian (Russia)"
......
This diff is collapsed.
...@@ -86,7 +86,7 @@ ...@@ -86,7 +86,7 @@
"Customize Ollama models for a specific purpose": "Personalize os modelos Ollama para um propósito específico", "Customize Ollama models for a specific purpose": "Personalize os modelos Ollama para um propósito específico",
"Dark": "Escuro", "Dark": "Escuro",
"Database": "Banco de dados", "Database": "Banco de dados",
"DD/MM/YYYY HH:mm": "DD/MM/AAAA HH:mm", "DD/MM/YYYY HH:mm": "DD/MM/YYYY HH:mm",
"Default": "Padrão", "Default": "Padrão",
"Default (Automatic1111)": "Padrão (Automatic1111)", "Default (Automatic1111)": "Padrão (Automatic1111)",
"Default (Web API)": "Padrão (API Web)", "Default (Web API)": "Padrão (API Web)",
......
...@@ -55,9 +55,9 @@ ...@@ -55,9 +55,9 @@
"Check for updates": "Kiểm tra cập nhật", "Check for updates": "Kiểm tra cập nhật",
"Checking for updates...": "Đang kiểm tra cập nhật...", "Checking for updates...": "Đang kiểm tra cập nhật...",
"Choose a model before saving...": "Chọn mô hình trước khi lưu...", "Choose a model before saving...": "Chọn mô hình trước khi lưu...",
"Chunk Overlap": "Kích thước chồng lấn (overlap)", "Chunk Overlap": "Chồng lấn (overlap)",
"Chunk Params": "Cài đặt số lượng ký tự cho khối ký tự (chunk)", "Chunk Params": "Cài đặt số lượng ký tự cho khối ký tự (chunk)",
"Chunk Size": "Kích thc khối (size)", "Chunk Size": "Kích thước khối (size)",
"Click here for help.": "Bấm vào đây để được trợ giúp.", "Click here for help.": "Bấm vào đây để được trợ giúp.",
"Click here to check other modelfiles.": "Bấm vào đây để kiểm tra các tệp mô tả mô hình (modelfiles) khác.", "Click here to check other modelfiles.": "Bấm vào đây để kiểm tra các tệp mô tả mô hình (modelfiles) khác.",
"Click here to select": "Bấm vào đây để chọn", "Click here to select": "Bấm vào đây để chọn",
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
"click here.": "bấm vào đây.", "click here.": "bấm vào đây.",
"Click on the user role button to change a user's role.": "Bấm vào nút trong cột VAI TRÒ để thay đổi quyền của người sử dụng.", "Click on the user role button to change a user's role.": "Bấm vào nút trong cột VAI TRÒ để thay đổi quyền của người sử dụng.",
"Close": "Đóng", "Close": "Đóng",
"Collection": "Bộ sưu tập", "Collection": "Tổng hợp mọi tài liệu",
"Command": "Lệnh", "Command": "Lệnh",
"Confirm Password": "Xác nhận Mật khẩu", "Confirm Password": "Xác nhận Mật khẩu",
"Connections": "Kết nối", "Connections": "Kết nối",
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
"Copy last response": "Sao chép phản hồi cuối cùng", "Copy last response": "Sao chép phản hồi cuối cùng",
"Copying to clipboard was successful!": "Sao chép vào clipboard thành công!", "Copying to clipboard was successful!": "Sao chép vào clipboard thành công!",
"Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Tạo một cụm từ súc tích, 3-5 từ làm tiêu đề cho truy vấn sau, tuân thủ nghiêm ngặt giới hạn 3-5 từ và tránh sử dụng từ 'tiêu đề':", "Create a concise, 3-5 word phrase as a header for the following query, strictly adhering to the 3-5 word limit and avoiding the use of the word 'title':": "Tạo một cụm từ súc tích, 3-5 từ làm tiêu đề cho truy vấn sau, tuân thủ nghiêm ngặt giới hạn 3-5 từ và tránh sử dụng từ 'tiêu đề':",
"Create a modelfile": "Tạo tệp mô tả mô hình", "Create a modelfile": "Tạo tệp mô tả cho mô hình",
"Create Account": "Tạo Tài khoản", "Create Account": "Tạo Tài khoản",
"Created at": "Được tạo vào lúc", "Created at": "Được tạo vào lúc",
"Created by": "Được tạo bởi", "Created by": "Được tạo bởi",
...@@ -347,7 +347,7 @@ ...@@ -347,7 +347,7 @@
"Valid time units:": "Đơn vị thời gian hợp lệ:", "Valid time units:": "Đơn vị thời gian hợp lệ:",
"variable": "biến", "variable": "biến",
"variable to have them replaced with clipboard content.": "biến để có chúng được thay thế bằng nội dung clipboard.", "variable to have them replaced with clipboard content.": "biến để có chúng được thay thế bằng nội dung clipboard.",
"Version": "Phiên bản", "Version": "Version",
"Web": "Web", "Web": "Web",
"WebUI Add-ons": "Tiện ích WebUI", "WebUI Add-ons": "Tiện ích WebUI",
"WebUI Settings": "Cài đặt WebUI", "WebUI Settings": "Cài đặt WebUI",
......
...@@ -467,3 +467,52 @@ export const blobToFile = (blob, fileName) => { ...@@ -467,3 +467,52 @@ export const blobToFile = (blob, fileName) => {
const file = new File([blob], fileName, { type: blob.type }); const file = new File([blob], fileName, { type: blob.type });
return file; return file;
}; };
// promptTemplate replaces any occurrences of the following in the template with the prompt
// {{prompt}} will be replaced with the prompt
// {{prompt:start:<length>}} will be replaced with the first <length> characters of the prompt
// {{prompt:end:<length>}} will be replaced with the last <length> characters of the prompt
// Character length is used as we don't have the ability to tokenize the prompt
export const promptTemplate = (template: string, prompt: string) => {
template = template.replace(/{{prompt}}/g, prompt);
// Replace all instances of {{prompt:start:<length>}} with the first <length> characters of the prompt
const startRegex = /{{prompt:start:(\d+)}}/g;
let startMatch: RegExpMatchArray | null;
while ((startMatch = startRegex.exec(template)) !== null) {
const length = parseInt(startMatch[1]);
template = template.replace(startMatch[0], prompt.substring(0, length));
}
// Replace all instances of {{prompt:end:<length>}} with the last <length> characters of the prompt
const endRegex = /{{prompt:end:(\d+)}}/g;
let endMatch: RegExpMatchArray | null;
while ((endMatch = endRegex.exec(template)) !== null) {
const length = parseInt(endMatch[1]);
template = template.replace(endMatch[0], prompt.substring(prompt.length - length));
}
return template;
};
export const approximateToHumanReadable = (nanoseconds: number) => {
const seconds = Math.floor((nanoseconds / 1e9) % 60);
const minutes = Math.floor((nanoseconds / 6e10) % 60);
const hours = Math.floor((nanoseconds / 3.6e12) % 24);
const results: string[] = [];
if (seconds >= 0) {
results.push(`${seconds}s`);
}
if (minutes > 0) {
results.push(`${minutes}m`);
}
if (hours > 0) {
results.push(`${hours}h`);
}
return results.reverse().join(' ');
};
...@@ -106,11 +106,6 @@ ...@@ -106,11 +106,6 @@
// IndexedDB Not Found // IndexedDB Not Found
} }
console.log();
await models.set(await getModels());
await tick();
await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}')); await settings.set(JSON.parse(localStorage.getItem('settings') ?? '{}'));
await modelfiles.set(await getModelfiles(localStorage.token)); await modelfiles.set(await getModelfiles(localStorage.token));
......
...@@ -849,9 +849,6 @@ ...@@ -849,9 +849,6 @@
shareEnabled={messages.length > 0} shareEnabled={messages.length > 0}
{chat} {chat}
{initNewChat} {initNewChat}
{tags}
{addTag}
{deleteTag}
/> />
<div class="flex flex-col flex-auto"> <div class="flex flex-col flex-auto">
<div <div
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths'; import { getSignUpEnabledStatus, toggleSignUpEnabledStatus } from '$lib/apis/auths';
import EditUserModal from '$lib/components/admin/EditUserModal.svelte'; import EditUserModal from '$lib/components/admin/EditUserModal.svelte';
import SettingsModal from '$lib/components/admin/SettingsModal.svelte'; import SettingsModal from '$lib/components/admin/SettingsModal.svelte';
import Pagination from '$lib/components/common/Pagination.svelte';
const i18n = getContext('i18n'); const i18n = getContext('i18n');
...@@ -21,6 +22,8 @@ ...@@ -21,6 +22,8 @@
let search = ''; let search = '';
let selectedUser = null; let selectedUser = null;
let page = 1;
let showSettingsModal = false; let showSettingsModal = false;
let showEditUserModal = false; let showEditUserModal = false;
...@@ -159,7 +162,8 @@ ...@@ -159,7 +162,8 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{#each users.filter((user) => { {#each users
.filter((user) => {
if (search === '') { if (search === '') {
return true; return true;
} else { } else {
...@@ -167,7 +171,8 @@ ...@@ -167,7 +171,8 @@
const query = search.toLowerCase(); const query = search.toLowerCase();
return name.includes(query); return name.includes(query);
} }
}) as user} })
.slice((page - 1) * 20, page * 20) as user}
<tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs"> <tr class="bg-white border-b dark:bg-gray-900 dark:border-gray-700 text-xs">
<td class="px-3 py-2 min-w-[7rem] w-28"> <td class="px-3 py-2 min-w-[7rem] w-28">
<button <button
...@@ -270,6 +275,8 @@ ...@@ -270,6 +275,8 @@
<div class=" text-gray-500 text-xs mt-2 text-right"> <div class=" text-gray-500 text-xs mt-2 text-right">
ⓘ {$i18n.t("Click on the user role button to change a user's role.")} ⓘ {$i18n.t("Click on the user role button to change a user's role.")}
</div> </div>
<Pagination bind:page count={users.length} />
</div> </div>
</div> </div>
</div> </div>
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment