Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
chenpangpang
open-webui
Commits
be5534c6
"src/lib/vscode:/vscode.git/clone" did not exist on "369816ace020ad2e822e49d95c76cfe264ae68af"
Unverified
Commit
be5534c6
authored
May 19, 2024
by
Timothy Jaeryang Baek
Committed by
GitHub
May 19, 2024
Browse files
Merge pull request #2376 from open-webui/dev
0.1.125
parents
90503be2
bcc2bab6
Changes
148
Hide whitespace changes
Inline
Side-by-side
Showing
20 changed files
with
2002 additions
and
436 deletions
+2002
-436
src/lib/components/chat/Settings/Interface.svelte
src/lib/components/chat/Settings/Interface.svelte
+72
-18
src/lib/components/chat/Settings/Personalization.svelte
src/lib/components/chat/Settings/Personalization.svelte
+96
-0
src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte
...nents/chat/Settings/Personalization/AddMemoryModal.svelte
+125
-0
src/lib/components/chat/Settings/Personalization/ManageModal.svelte
...mponents/chat/Settings/Personalization/ManageModal.svelte
+165
-0
src/lib/components/chat/SettingsModal.svelte
src/lib/components/chat/SettingsModal.svelte
+25
-1
src/lib/components/chat/ShareChatModal.svelte
src/lib/components/chat/ShareChatModal.svelte
+16
-2
src/lib/components/common/Modal.svelte
src/lib/components/common/Modal.svelte
+2
-2
src/lib/components/common/Selector.svelte
src/lib/components/common/Selector.svelte
+1
-1
src/lib/components/common/Switch.svelte
src/lib/components/common/Switch.svelte
+22
-0
src/lib/components/documents/Settings/General.svelte
src/lib/components/documents/Settings/General.svelte
+4
-2
src/lib/components/documents/Settings/QueryParams.svelte
src/lib/components/documents/Settings/QueryParams.svelte
+1
-1
src/lib/components/icons/MenuLines.svelte
src/lib/components/icons/MenuLines.svelte
+19
-0
src/lib/components/icons/User.svelte
src/lib/components/icons/User.svelte
+11
-0
src/lib/components/layout/Navbar.svelte
src/lib/components/layout/Navbar.svelte
+65
-37
src/lib/components/layout/Navbar/Menu.svelte
src/lib/components/layout/Navbar/Menu.svelte
+62
-88
src/lib/components/layout/Sidebar.svelte
src/lib/components/layout/Sidebar.svelte
+133
-280
src/lib/components/layout/Sidebar/ChatMenu.svelte
src/lib/components/layout/Sidebar/ChatMenu.svelte
+16
-4
src/lib/components/layout/Sidebar/UserMenu.svelte
src/lib/components/layout/Sidebar/UserMenu.svelte
+147
-0
src/lib/components/workspace/Documents.svelte
src/lib/components/workspace/Documents.svelte
+611
-0
src/lib/components/workspace/Modelfiles.svelte
src/lib/components/workspace/Modelfiles.svelte
+409
-0
No files found.
src/lib/components/chat/Settings/Interface.svelte
View file @
be5534c6
...
@@ -22,6 +22,8 @@
...
@@ -22,6 +22,8 @@
// Interface
// Interface
let promptSuggestions = [];
let promptSuggestions = [];
let showUsername = false;
let showUsername = false;
let chatBubble = true;
let chatDirection: 'LTR' | 'RTL' = 'LTR';
const toggleSplitLargeChunks = async () => {
const toggleSplitLargeChunks = async () => {
splitLargeChunks = !splitLargeChunks;
splitLargeChunks = !splitLargeChunks;
...
@@ -33,6 +35,11 @@
...
@@ -33,6 +35,11 @@
saveSettings({ fullScreenMode: fullScreenMode });
saveSettings({ fullScreenMode: fullScreenMode });
};
};
const toggleChatBubble = async () => {
chatBubble = !chatBubble;
saveSettings({ chatBubble: chatBubble });
};
const toggleShowUsername = async () => {
const toggleShowUsername = async () => {
showUsername = !showUsername;
showUsername = !showUsername;
saveSettings({ showUsername: showUsername });
saveSettings({ showUsername: showUsername });
...
@@ -70,6 +77,11 @@
...
@@ -70,6 +77,11 @@
}
}
};
};
const toggleChangeChatDirection = async () => {
chatDirection = chatDirection === 'LTR' ? 'RTL' : 'LTR';
saveSettings({ chatDirection });
};
const updateInterfaceHandler = async () => {
const updateInterfaceHandler = async () => {
if ($user.role === 'admin') {
if ($user.role === 'admin') {
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
promptSuggestions = await setDefaultPromptSuggestions(localStorage.token, promptSuggestions);
...
@@ -105,8 +117,10 @@
...
@@ -105,8 +117,10 @@
responseAutoCopy = settings.responseAutoCopy ?? false;
responseAutoCopy = settings.responseAutoCopy ?? false;
showUsername = settings.showUsername ?? false;
showUsername = settings.showUsername ?? false;
chatBubble = settings.chatBubble ?? true;
fullScreenMode = settings.fullScreenMode ?? false;
fullScreenMode = settings.fullScreenMode ?? false;
splitLargeChunks = settings.splitLargeChunks ?? false;
splitLargeChunks = settings.splitLargeChunks ?? false;
chatDirection = settings.chatDirection ?? 'LTR';
});
});
</script>
</script>
...
@@ -117,22 +131,22 @@
...
@@ -117,22 +131,22 @@
dispatch('save');
dispatch('save');
}}
}}
>
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[2
2
rem]">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[2
5
rem]">
<div>
<div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
<div class=" mb-1 text-sm font-medium">{$i18n.t('WebUI Add-ons')}</div>
<div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('
Title Auto-Generation
')}</div>
<div class=" self-center text-xs font-medium">{$i18n.t('
Chat Bubble UI
')}</div>
<button
<button
class="p-1 px-3 text-xs flex rounded transition"
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
on:click={() => {
toggle
TitleAutoGenerat
e();
toggle
ChatBubbl
e();
}}
}}
type="button"
type="button"
>
>
{#if
titleAutoGenerat
e === true}
{#if
chatBubbl
e === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
...
@@ -143,18 +157,16 @@
...
@@ -143,18 +157,16 @@
<div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
<div class=" self-center text-xs font-medium">{$i18n.t('Title Auto-Generation')}</div>
{$i18n.t('Response AutoCopy to Clipboard')}
</div>
<button
<button
class="p-1 px-3 text-xs flex rounded transition"
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
on:click={() => {
toggle
ResponseAutoCopy
();
toggle
TitleAutoGenerate
();
}}
}}
type="button"
type="button"
>
>
{#if
responseAutoCopy
=== true}
{#if
titleAutoGenerate
=== true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
...
@@ -165,16 +177,18 @@
...
@@ -165,16 +177,18 @@
<div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
<div class=" self-center text-xs font-medium">
{$i18n.t('Response AutoCopy to Clipboard')}
</div>
<button
<button
class="p-1 px-3 text-xs flex rounded transition"
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
on:click={() => {
toggle
FullScreenMode
();
toggle
ResponseAutoCopy
();
}}
}}
type="button"
type="button"
>
>
{#if
fullScreenMode
=== true}
{#if
responseAutoCopy
=== true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
...
@@ -185,18 +199,16 @@
...
@@ -185,18 +199,16 @@
<div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
<div class=" self-center text-xs font-medium">{$i18n.t('Full Screen Mode')}</div>
{$i18n.t('Display the username instead of You in the Chat')}
</div>
<button
<button
class="p-1 px-3 text-xs flex rounded transition"
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
on:click={() => {
toggle
ShowUsernam
e();
toggle
FullScreenMod
e();
}}
}}
type="button"
type="button"
>
>
{#if
showUsernam
e === true}
{#if
fullScreenMod
e === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
...
@@ -205,6 +217,30 @@
...
@@ -205,6 +217,30 @@
</div>
</div>
</div>
</div>
{#if !$settings.chatBubble}
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
{$i18n.t('Display the username instead of You in the Chat')}
</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={() => {
toggleShowUsername();
}}
type="button"
>
{#if showUsername === true}
<span class="ml-2 self-center">{$i18n.t('On')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('Off')}</span>
{/if}
</button>
</div>
</div>
{/if}
<div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">
<div class=" self-center text-xs font-medium">
...
@@ -228,6 +264,24 @@
...
@@ -228,6 +264,24 @@
</div>
</div>
</div>
</div>
<div>
<div class=" py-0.5 flex w-full justify-between">
<div class=" self-center text-xs font-medium">{$i18n.t('Chat direction')}</div>
<button
class="p-1 px-3 text-xs flex rounded transition"
on:click={toggleChangeChatDirection}
type="button"
>
{#if chatDirection === 'LTR'}
<span class="ml-2 self-center">{$i18n.t('LTR')}</span>
{:else}
<span class="ml-2 self-center">{$i18n.t('RTL')}</span>
{/if}
</button>
</div>
</div>
<hr class=" dark:border-gray-700" />
<hr class=" dark:border-gray-700" />
<div>
<div>
...
@@ -283,7 +337,7 @@
...
@@ -283,7 +337,7 @@
{#if $user.role === 'admin'}
{#if $user.role === 'admin'}
<hr class=" dark:border-gray-700" />
<hr class=" dark:border-gray-700" />
<div class=" space-y-3 pr-1.5
overflow-y-scroll max-h-80
">
<div class=" space-y-3 pr-1.5">
<div class="flex w-full justify-between mb-2">
<div class="flex w-full justify-between mb-2">
<div class=" self-center text-sm font-semibold">
<div class=" self-center text-sm font-semibold">
{$i18n.t('Default Prompt Suggestions')}
{$i18n.t('Default Prompt Suggestions')}
...
...
src/lib/components/chat/Settings/Personalization.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
import { getBackendConfig } from '$lib/apis';
import { setDefaultPromptSuggestions } from '$lib/apis/configs';
import Switch from '$lib/components/common/Switch.svelte';
import { config, models, settings, user } from '$lib/stores';
import { createEventDispatcher, onMount, getContext, tick } from 'svelte';
import { toast } from 'svelte-sonner';
import ManageModal from './Personalization/ManageModal.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
const dispatch = createEventDispatcher();
const i18n = getContext('i18n');
export let saveSettings: Function;
let showManageModal = false;
// Addons
let enableMemory = false;
onMount(async () => {
let settings = JSON.parse(localStorage.getItem('settings') ?? '{}');
enableMemory = settings?.memory ?? false;
});
</script>
<ManageModal bind:show={showManageModal} />
<form
class="flex flex-col h-full justify-between space-y-3 text-sm"
on:submit|preventDefault={() => {
dispatch('save');
}}
>
<div class=" pr-1.5 overflow-y-scroll max-h-[25rem]">
<div>
<div class="flex items-center justify-between mb-1">
<Tooltip
content="This is an experimental feature, it may not function as expected and is subject to change at any time."
>
<div class="text-sm font-medium">
{$i18n.t('Memory')}
<span class=" text-xs text-gray-500">({$i18n.t('Experimental')})</span>
</div>
</Tooltip>
<div class="mt-1">
<Switch
bind:state={enableMemory}
on:change={async () => {
saveSettings({ memory: enableMemory });
}}
/>
</div>
</div>
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
<div>
You can personalize your interactions with LLMs by adding memories through the 'Manage'
button below, making them more helpful and tailored to you.
</div>
<!-- <div class="mt-3">
To understand what LLM remembers or teach it something new, just chat with it:
<div>- “Remember that I like concise responses.”</div>
<div>- “I just got a puppy!”</div>
<div>- “What do you remember about me?”</div>
<div>- “Where did we leave off on my last project?”</div>
</div> -->
</div>
<div class="mt-3 mb-1 ml-1">
<button
type="button"
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
on:click={() => {
showManageModal = true;
}}
>
Manage
</button>
</div>
</div>
<div class="flex justify-end text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
type="submit"
>
{$i18n.t('Save')}
</button>
</div>
</form>
src/lib/components/chat/Settings/Personalization/AddMemoryModal.svelte
0 → 100644
View file @
be5534c6
<script>
import { createEventDispatcher, getContext } from 'svelte';
import Modal from '$lib/components/common/Modal.svelte';
import { addNewMemory } from '$lib/apis/memories';
import { toast } from 'svelte-sonner';
const dispatch = createEventDispatcher();
export let show;
const i18n = getContext('i18n');
let loading = false;
let content = '';
const submitHandler = async () => {
loading = true;
const res = await addNewMemory(localStorage.token, content).catch((error) => {
toast.error(error);
return null;
});
if (res) {
console.log(res);
toast.success('Memory added successfully');
content = '';
show = false;
dispatch('save');
}
loading = false;
};
</script>
<Modal bind:show size="sm">
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-2">
<div class=" text-lg font-medium self-center">{$i18n.t('Add Memory')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col md:flex-row w-full px-5 pb-4 md:space-x-4 dark:text-gray-200">
<div class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6">
<form
class="flex flex-col w-full"
on:submit|preventDefault={() => {
submitHandler();
}}
>
<div class="">
<textarea
bind:value={content}
class=" bg-transparent w-full text-sm resize-none rounded-xl p-3 outline outline-1 outline-gray-100 dark:outline-gray-800"
rows="3"
placeholder={$i18n.t('Enter a detail about yourself for your LLMs to recall')}
/>
<div class="text-xs text-gray-500">
ⓘ Refer to yourself as "User" (e.g., "User is learning Spanish")
</div>
</div>
<div class="flex justify-end pt-1 text-sm font-medium">
<button
class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-3xl flex flex-row space-x-1 items-center {loading
? ' cursor-not-allowed'
: ''}"
type="submit"
disabled={loading}
>
{$i18n.t('Add')}
{#if loading}
<div class="ml-2 self-center">
<svg
class=" w-4 h-4"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_ajPY {
transform-origin: center;
animation: spinner_AtaB 0.75s infinite linear;
}
@keyframes spinner_AtaB {
100% {
transform: rotate(360deg);
}
}
</style><path
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
opacity=".25"
/><path
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
class="spinner_ajPY"
/></svg
>
</div>
{/if}
</button>
</div>
</form>
</div>
</div>
</div>
</Modal>
src/lib/components/chat/Settings/Personalization/ManageModal.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
import { toast } from 'svelte-sonner';
import dayjs from 'dayjs';
import { getContext, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
import Modal from '$lib/components/common/Modal.svelte';
import AddMemoryModal from './AddMemoryModal.svelte';
import { deleteMemoriesByUserId, deleteMemoryById, getMemories } from '$lib/apis/memories';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import { error } from '@sveltejs/kit';
const i18n = getContext('i18n');
export let show = false;
let memories = [];
let showAddMemoryModal = false;
$: if (show) {
(async () => {
memories = await getMemories(localStorage.token);
})();
}
</script>
<Modal size="xl" bind:show>
<div>
<div class=" flex justify-between dark:text-gray-300 px-5 pt-4 pb-1">
<div class=" text-lg font-medium self-center">{$i18n.t('Memory')}</div>
<button
class="self-center"
on:click={() => {
show = false;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"
/>
</svg>
</button>
</div>
<div class="flex flex-col w-full px-5 pb-5 dark:text-gray-200">
<div
class=" flex flex-col w-full sm:flex-row sm:justify-center sm:space-x-6 h-[28rem] max-h-screen outline outline-1 rounded-xl outline-gray-100 dark:outline-gray-800 mb-4 mt-1"
>
{#if memories.length > 0}
<div class="text-left text-sm w-full mb-4 max-h-[22rem] overflow-y-scroll">
<div class="relative overflow-x-auto">
<table class="w-full text-sm text-left text-gray-600 dark:text-gray-400 table-auto">
<thead
class="text-xs text-gray-700 uppercase bg-transparent dark:text-gray-200 border-b-2 dark:border-gray-800"
>
<tr>
<th scope="col" class="px-3 py-2"> {$i18n.t('Name')} </th>
<th scope="col" class="px-3 py-2 hidden md:flex"> {$i18n.t('Created At')} </th>
<th scope="col" class="px-3 py-2 text-right" />
</tr>
</thead>
<tbody>
{#each memories as memory}
<tr class="border-b dark:border-gray-800 items-center">
<td class="px-3 py-1">
<div class="line-clamp-1">
{memory.content}
</div>
</td>
<td class=" px-3 py-1 hidden md:flex h-[2.5rem]">
<div class="my-auto whitespace-nowrap">
{dayjs(memory.created_at * 1000).format($i18n.t('MMMM DD, YYYY'))}
</div>
</td>
<td class="px-3 py-1">
<div class="flex justify-end w-full">
<Tooltip content="Delete">
<button
class="self-center w-fit text-sm px-2 py-2 hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
on:click={async () => {
const res = await deleteMemoryById(
localStorage.token,
memory.id
).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Memory deleted successfully');
memories = await getMemories(localStorage.token);
}
}}
>
<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="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</Tooltip>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{:else}
<div class="text-center flex h-full text-sm w-full">
<div class=" my-auto pb-10 px-4 w-full text-gray-500">
{$i18n.t('Memories accessible by LLMs will be shown here.')}
</div>
</div>
{/if}
</div>
<div class="flex text-sm font-medium gap-1.5">
<button
class=" px-3.5 py-1.5 font-medium hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-gray-300 dark:outline-gray-800 rounded-3xl"
on:click={() => {
showAddMemoryModal = true;
}}>Add memory</button
>
<button
class=" px-3.5 py-1.5 font-medium text-red-500 hover:bg-black/5 dark:hover:bg-white/5 outline outline-1 outline-red-300 dark:outline-red-800 rounded-3xl"
on:click={async () => {
const res = await deleteMemoriesByUserId(localStorage.token).catch((error) => {
toast.error(error);
return null;
});
if (res) {
toast.success('Memory cleared successfully');
memories = [];
}
}}>Clear memory</button
>
</div>
</div>
</div>
</Modal>
<AddMemoryModal
bind:show={showAddMemoryModal}
on:save={async () => {
memories = await getMemories(localStorage.token);
}}
/>
src/lib/components/chat/SettingsModal.svelte
View file @
be5534c6
...
@@ -15,6 +15,8 @@
...
@@ -15,6 +15,8 @@
import Chats from './Settings/Chats.svelte';
import Chats from './Settings/Chats.svelte';
import Connections from './Settings/Connections.svelte';
import Connections from './Settings/Connections.svelte';
import Images from './Settings/Images.svelte';
import Images from './Settings/Images.svelte';
import User from '../icons/User.svelte';
import Personalization from './Settings/Personalization.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -165,6 +167,21 @@
...
@@ -165,6 +167,21 @@
<div class=" self-center">{$i18n.t('Interface')}</div>
<div class=" self-center">{$i18n.t('Interface')}</div>
</button>
</button>
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'personalization'
? 'bg-gray-200 dark:bg-gray-700'
: ' hover:bg-gray-300 dark:hover:bg-gray-800'}"
on:click={() => {
selectedTab = 'personalization';
}}
>
<div class=" self-center mr-2">
<User />
</div>
<div class=" self-center">{$i18n.t('Personalization')}</div>
</button>
<button
<button
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
class="px-2.5 py-2.5 min-w-fit rounded-lg flex-1 md:flex-none flex text-right transition {selectedTab ===
'audio'
'audio'
...
@@ -298,7 +315,7 @@
...
@@ -298,7 +315,7 @@
<div class=" self-center">{$i18n.t('About')}</div>
<div class=" self-center">{$i18n.t('About')}</div>
</button>
</button>
</div>
</div>
<div class="flex-1 md:min-h-[2
5
rem]">
<div class="flex-1 md:min-h-[2
8
rem]">
{#if selectedTab === 'general'}
{#if selectedTab === 'general'}
<General
<General
{getModels}
{getModels}
...
@@ -323,6 +340,13 @@
...
@@ -323,6 +340,13 @@
toast.success($i18n.t('Settings saved successfully!'));
toast.success($i18n.t('Settings saved successfully!'));
}}
}}
/>
/>
{:else if selectedTab === 'personalization'}
<Personalization
{saveSettings}
on:save={() => {
toast.success($i18n.t('Settings saved successfully!'));
}}
/>
{:else if selectedTab === 'audio'}
{:else if selectedTab === 'audio'}
<Audio
<Audio
{saveSettings}
{saveSettings}
...
...
src/lib/components/chat/ShareChatModal.svelte
View file @
be5534c6
...
@@ -57,10 +57,23 @@
...
@@ -57,10 +57,23 @@
export let show = false;
export let show = false;
const isDifferentChat = (_chat) => {
if (!chat) {
return true;
}
if (!_chat) {
return false;
}
return chat.id !== _chat.id || chat.share_id !== _chat.share_id;
};
$: if (show) {
$: if (show) {
(async () => {
(async () => {
if (chatId) {
if (chatId) {
chat = await getChatById(localStorage.token, chatId);
const _chat = await getChatById(localStorage.token, chatId);
if (isDifferentChat(_chat)) {
chat = _chat;
}
} else {
} else {
chat = null;
chat = null;
console.log(chat);
console.log(chat);
...
@@ -115,7 +128,7 @@
...
@@ -115,7 +128,7 @@
{$i18n.t('and create a new shared link.')}
{$i18n.t('and create a new shared link.')}
{:else}
{:else}
{$i18n.t(
{$i18n.t(
"Messages you send after creating your link won't be shared. Users with the URL will beable to view the shared chat."
"Messages you send after creating your link won't be shared. Users with the URL will be
able to view the shared chat."
)}
)}
{/if}
{/if}
</div>
</div>
...
@@ -137,6 +150,7 @@
...
@@ -137,6 +150,7 @@
<button
<button
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
class=" self-center flex items-center gap-1 px-3.5 py-2 rounded-xl text-sm font-medium bg-emerald-600 hover:bg-emerald-500 text-white"
type="button"
type="button"
id="copy-and-share-chat-button"
on:click={async () => {
on:click={async () => {
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
...
...
src/lib/components/common/Modal.svelte
View file @
be5534c6
...
@@ -16,9 +16,9 @@
...
@@ -16,9 +16,9 @@
} else if (size === 'sm') {
} else if (size === 'sm') {
return 'w-[30rem]';
return 'w-[30rem]';
} else if (size === 'md') {
} else if (size === 'md') {
return 'w-[44rem]';
} else {
return 'w-[48rem]';
return 'w-[48rem]';
} else {
return 'w-[56rem]';
}
}
};
};
...
...
src/lib/components/common/Selector.svelte
View file @
be5534c6
...
@@ -26,7 +26,7 @@
...
@@ -26,7 +26,7 @@
let searchValue = '';
let searchValue = '';
$: filteredItems = searchValue
$: filteredItems = searchValue
? items.filter((item) => item.value.includes(searchValue.toLowerCase()))
? items.filter((item) => item.value.
toLowerCase().
includes(searchValue.toLowerCase()))
: items;
: items;
</script>
</script>
...
...
src/lib/components/common/Switch.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
import { createEventDispatcher, tick } from 'svelte';
import { Switch } from 'bits-ui';
export let state = true;
const dispatch = createEventDispatcher();
</script>
<Switch.Root
bind:checked={state}
onCheckedChange={async (e) => {
await tick();
dispatch('change', e);
}}
class="flex h-5 min-h-5 w-9 shrink-0 cursor-pointer items-center rounded-full px-[3px] transition {state
? ' bg-emerald-600'
: 'bg-gray-200 dark:bg-transparent'} outline outline-1 outline-gray-100 dark:outline-gray-800"
>
<Switch.Thumb
class="pointer-events-none block size-4 shrink-0 rounded-full bg-white transition-transform data-[state=checked]:translate-x-3.5 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini "
/>
</Switch.Root>
src/lib/components/documents/Settings/General.svelte
View file @
be5534c6
...
@@ -190,13 +190,13 @@
...
@@ -190,13 +190,13 @@
saveHandler();
saveHandler();
}}
}}
>
>
<div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[2
2
rem]">
<div class=" space-y-2.5 pr-1.5 overflow-y-scroll max-h-[2
8
rem]">
<div class="flex flex-col gap-0.5">
<div class="flex flex-col gap-0.5">
<div class=" mb-0.5 text-sm font-medium">{$i18n.t('General Settings')}</div>
<div class=" mb-0.5 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('Scan for documents from {{path}}', { path: '/data/docs' })}
{$i18n.t('Scan for documents from {{path}}', { path: '
DOCS_DIR (
/data/docs
)
' })}
</div>
</div>
<button
<button
...
@@ -254,6 +254,8 @@
...
@@ -254,6 +254,8 @@
embeddingModel = '';
embeddingModel = '';
} else if (e.target.value === 'openai') {
} else if (e.target.value === 'openai') {
embeddingModel = 'text-embedding-3-small';
embeddingModel = 'text-embedding-3-small';
} else if (e.target.value === '') {
embeddingModel = 'sentence-transformers/all-MiniLM-L6-v2';
}
}
}}
}}
>
>
...
...
src/lib/components/documents/Settings/QueryParams.svelte
View file @
be5534c6
...
@@ -46,7 +46,7 @@
...
@@ -46,7 +46,7 @@
saveHandler();
saveHandler();
}}
}}
>
>
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[2
2
rem]">
<div class=" space-y-3 pr-1.5 overflow-y-scroll max-h-[2
5
rem]">
<div class=" ">
<div class=" ">
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
<div class=" text-sm font-medium">{$i18n.t('Query Params')}</div>
...
...
src/lib/components/icons/MenuLines.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
export let className = 'size-5';
export let strokeWidth = '2';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width={strokeWidth}
stroke="currentColor"
class={className}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
/>
</svg>
src/lib/components/icons/User.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
export let className = 'w-4 h-4';
</script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class={className}>
<path
fill-rule="evenodd"
d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z"
clip-rule="evenodd"
/>
</svg>
src/lib/components/layout/Navbar.svelte
View file @
be5534c6
...
@@ -2,7 +2,17 @@
...
@@ -2,7 +2,17 @@
import { getContext } from 'svelte';
import { getContext } from 'svelte';
import { toast } from 'svelte-sonner';
import { toast } from 'svelte-sonner';
import { WEBUI_NAME, chatId, modelfiles, settings, showSettings } from '$lib/stores';
import {
WEBUI_NAME,
chatId,
mobile,
modelfiles,
settings,
showArchivedChats,
showSettings,
showSidebar,
user
} from '$lib/stores';
import { slide } from 'svelte/transition';
import { slide } from 'svelte/transition';
import ShareChatModal from '../chat/ShareChatModal.svelte';
import ShareChatModal from '../chat/ShareChatModal.svelte';
...
@@ -10,6 +20,8 @@
...
@@ -10,6 +20,8 @@
import Tooltip from '../common/Tooltip.svelte';
import Tooltip from '../common/Tooltip.svelte';
import Menu from './Navbar/Menu.svelte';
import Menu from './Navbar/Menu.svelte';
import { page } from '$app/stores';
import { page } from '$app/stores';
import UserMenu from './Sidebar/UserMenu.svelte';
import MenuLines from '../icons/MenuLines.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -28,48 +40,35 @@
...
@@ -28,48 +40,35 @@
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
<ShareChatModal bind:show={showShareChatModal} chatId={$chatId} />
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<nav id="nav" class=" sticky py-2.5 top-0 flex flex-row justify-center z-30">
<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1
.3
rem]">
<div class=" flex max-w-full w-full mx-auto px-5 pt-0.5 md:px-[1rem]">
<div class="flex items-center w-full max-w-full">
<div class="flex items-center w-full max-w-full">
<div
class="{$showSidebar
? 'md:hidden'
: ''} mr-3 self-start flex flex-none items-center text-gray-600 dark:text-gray-400"
>
<button
id="sidebar-toggle-button"
class="cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
on:click={() => {
showSidebar.set(!$showSidebar);
}}
>
<div class=" m-auto self-center">
<MenuLines />
</div>
</button>
</div>
<div class="flex-1 overflow-hidden max-w-full">
<div class="flex-1 overflow-hidden max-w-full">
{#if showModelSelector}
{#if showModelSelector}
<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
<ModelSelector bind:selectedModels showSetDefault={!shareEnabled} />
{/if}
{/if}
</div>
</div>
<div class="self-start flex flex-none items-center">
<div class="self-start flex flex-none items-center
text-gray-600 dark:text-gray-400
">
<div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
<!--
<div class="md:hidden flex self-center w-[1px] h-5 mx-2 bg-gray-300 dark:bg-stone-700" />
-->
{#if !shareEnabled}
{#if shareEnabled}
<Tooltip content={$i18n.t('Settings')}>
<button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
id="open-settings-button"
on:click={async () => {
await showSettings.set(!$showSettings);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
</Tooltip>
{:else}
<Menu
<Menu
{chat}
{chat}
{shareEnabled}
{shareEnabled}
...
@@ -81,7 +80,8 @@
...
@@ -81,7 +80,8 @@
}}
}}
>
>
<button
<button
class="cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
class="hidden md:flex cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
id="chat-context-menu-button"
>
>
<div class=" m-auto self-center">
<div class=" m-auto self-center">
<svg
<svg
...
@@ -105,7 +105,9 @@
...
@@ -105,7 +105,9 @@
<Tooltip content={$i18n.t('New Chat')}>
<Tooltip content={$i18n.t('New Chat')}>
<button
<button
id="new-chat-button"
id="new-chat-button"
class=" cursor-pointer p-1.5 flex dark:hover:bg-gray-700 rounded-full transition"
class=" flex {$showSidebar
? 'md:hidden'
: ''} cursor-pointer px-2 py-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
on:click={() => {
on:click={() => {
initNewChat();
initNewChat();
}}
}}
...
@@ -127,6 +129,32 @@
...
@@ -127,6 +129,32 @@
</div>
</div>
</button>
</button>
</Tooltip>
</Tooltip>
{#if $user !== undefined}
<UserMenu
className="max-w-[200px]"
role={$user.role}
on:show={(e) => {
if (e.detail === 'archived-chat') {
showArchivedChats.set(true);
}
}}
>
<button
class="select-none flex rounded-xl p-1.5 w-full hover:bg-gray-100 dark:hover:bg-gray-850 transition"
aria-label="User Menu"
>
<div class=" self-center">
<img
src={$user.profile_image_url}
class="size-6 object-cover rounded-full"
alt="User profile"
draggable="false"
/>
</div>
</button>
</UserMenu>
{/if}
</div>
</div>
</div>
</div>
</div>
</div>
...
...
src/lib/components/layout/Navbar/Menu.svelte
View file @
be5534c6
...
@@ -76,14 +76,14 @@
...
@@ -76,14 +76,14 @@
<div slot="content">
<div slot="content">
<DropdownMenu.Content
<DropdownMenu.Content
class="w-full max-w-[200px] rounded-l
g
px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-
90
0 dark:text-white shadow-lg"
class="w-full max-w-[200px] rounded-
x
l px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-
85
0 dark:text-white shadow-lg"
sideOffset={8}
sideOffset={8}
side="bottom"
side="bottom"
align="end"
align="end"
transition={flyAndScale}
transition={flyAndScale}
>
>
<DropdownMenu.Item
<!--
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-8
5
0 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-8
0
0 rounded-md"
on:click={async () => {
on:click={async () => {
await showSettings.set(!$showSettings);
await showSettings.set(!$showSettings);
}}
}}
...
@@ -108,113 +108,87 @@
...
@@ -108,113 +108,87 @@
/>
/>
</svg>
</svg>
<div class="flex items-center">{$i18n.t('Settings')}</div>
<div class="flex items-center">{$i18n.t('Settings')}</div>
</DropdownMenu.Item>
</DropdownMenu.Item>
-->
{#if shareEnabled}
<DropdownMenu.Item
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
id="chat-share-button"
on:click={() => {
on:click={() => {
shareHandler();
shareHandler();
}}
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="size-4"
>
>
<svg
<path
xmlns="http://www.w3.org/2000/svg"
fill-rule="evenodd"
viewBox="0 0 24 24"
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
fill="currentColor"
clip-rule="evenodd"
class="size-4"
/>
>
</svg>
<path
<div class="flex items-center">{$i18n.t('Share')}</div>
fill-rule="evenodd"
</DropdownMenu.Item>
d="M15.75 4.5a3 3 0 1 1 .825 2.066l-8.421 4.679a3.002 3.002 0 0 1 0 1.51l8.421 4.679a3 3 0 1 1-.729 1.31l-8.421-4.678a3 3 0 1 1 0-4.132l8.421-4.679a3 3 0 0 1-.096-.755Z"
clip-rule="evenodd"
/>
</svg>
<div class="flex items-center">{$i18n.t('Share')}</div>
</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"
on:click={() => {
on:click={() => {
downloadHandler();
downloadHandler();
}}
}}
/> -->
/> -->
<DropdownMenu.Sub>
<DropdownMenu.Sub>
<DropdownMenu.SubTrigger
<DropdownMenu.SubTrigger
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3"
/>
</svg>
<div class="flex items-center">{$i18n.t('Download')}</div>
</DropdownMenu.SubTrigger>
<DropdownMenu.SubContent
class="w-full 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-lg"
transition={flyAndScale}
sideOffset={8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
downloadTxt();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer dark:hover:bg-gray-850 rounded-md"
on:click={() => {
downloadPdf();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags chatId={chat.id} />
</div>
<!-- <DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer"
on:click={() => {
tagHandler();
}}
>
>
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
fill="none"
fill="none"
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="
2
"
stroke-width="
1.5
"
stroke="currentColor"
stroke="currentColor"
class="size-4"
class="size-4"
>
>
<path
<path
stroke-linecap="round"
stroke-linecap="round"
stroke-linejoin="round"
stroke-linejoin="round"
d="M
9.568 3H5
.25A2.25 2.25 0 0 0
3
5.25
v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 0 0 5.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 0 0 9.568 3Z
"
d="M
3 16.5v2
.25A2.25 2.25 0 0 0 5.25
21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3
"
/>
/>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6Z" />
</svg>
</svg>
<div class="flex items-center">Tag</div>
<div class="flex items-center">{$i18n.t('Download')}</div>
</DropdownMenu.Item> -->
</DropdownMenu.SubTrigger>
{/if}
<DropdownMenu.SubContent
class="w-full 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-lg"
transition={flyAndScale}
sideOffset={8}
>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadTxt();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('Plain text (.txt)')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
downloadPdf();
}}
>
<div class="flex items-center line-clamp-1">{$i18n.t('PDF document (.pdf)')}</div>
</DropdownMenu.Item>
</DropdownMenu.SubContent>
</DropdownMenu.Sub>
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
<div class="flex p-1">
<Tags chatId={chat.id} />
</div>
</DropdownMenu.Content>
</DropdownMenu.Content>
</div>
</div>
</Dropdown>
</Dropdown>
src/lib/components/layout/Sidebar.svelte
View file @
be5534c6
<script lang="ts">
<script lang="ts">
import { goto } from '$app/navigation';
import { goto } from '$app/navigation';
import { user, chats, settings, showSettings, chatId, tags, showSidebar } from '$lib/stores';
import {
user,
chats,
settings,
showSettings,
chatId,
tags,
showSidebar,
mobile,
showArchivedChats
} from '$lib/stores';
import { onMount, getContext } from 'svelte';
import { onMount, getContext } from 'svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
...
@@ -22,8 +32,9 @@
...
@@ -22,8 +32,9 @@
import ShareChatModal from '../chat/ShareChatModal.svelte';
import ShareChatModal from '../chat/ShareChatModal.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte';
import ArchiveBox from '../icons/ArchiveBox.svelte';
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
import ArchivedChatsModal from './Sidebar/ArchivedChatsModal.svelte';
import UserMenu from './Sidebar/UserMenu.svelte';
const BREAKPOINT =
1024
;
const BREAKPOINT =
768
;
let show = false;
let show = false;
let navElement;
let navElement;
...
@@ -39,7 +50,6 @@
...
@@ -39,7 +50,6 @@
let chatTitleEditId = null;
let chatTitleEditId = null;
let chatTitle = '';
let chatTitle = '';
let showArchivedChatsModal = false;
let showShareChatModal = false;
let showShareChatModal = false;
let showDropdown = false;
let showDropdown = false;
let isEditing = false;
let isEditing = false;
...
@@ -66,7 +76,24 @@
...
@@ -66,7 +76,24 @@
}
}
});
});
mobile;
const onResize = () => {
if ($showSidebar && window.innerWidth < BREAKPOINT) {
showSidebar.set(false);
}
};
onMount(async () => {
onMount(async () => {
mobile.subscribe((e) => {
if ($showSidebar && e) {
showSidebar.set(false);
}
if (!$showSidebar && !e) {
showSidebar.set(true);
}
});
showSidebar.set(window.innerWidth > BREAKPOINT);
showSidebar.set(window.innerWidth > BREAKPOINT);
await chats.set(await getChatList(localStorage.token));
await chats.set(await getChatList(localStorage.token));
...
@@ -96,20 +123,12 @@
...
@@ -96,20 +123,12 @@
checkDirection();
checkDirection();
};
};
const onResize = () => {
if ($showSidebar && window.innerWidth < BREAKPOINT) {
showSidebar.set(false);
}
};
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('touchend', onTouchEnd);
window.addEventListener('resize', onResize);
return () => {
return () => {
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('touchend', onTouchEnd);
window.removeEventListener('resize', onResize);
};
};
});
});
...
@@ -176,31 +195,43 @@
...
@@ -176,31 +195,43 @@
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ShareChatModal bind:show={showShareChatModal} chatId={shareChatId} />
<ArchivedChatsModal
<ArchivedChatsModal
bind:show={showArchivedChats
Modal
}
bind:show={
$
showArchivedChats}
on:change={async () => {
on:change={async () => {
await chats.set(await getChatList(localStorage.token));
await chats.set(await getChatList(localStorage.token));
}}
}}
/>
/>
<!-- svelte-ignore a11y-no-static-element-interactions -->
{#if $showSidebar}
<div
class=" fixed md:hidden z-40 top-0 right-0 left-0 bottom-0 bg-black/60 w-full min-h-screen h-screen flex justify-center overflow-hidden overscroll-contain"
on:mousedown={() => {
showSidebar.set(!$showSidebar);
}}
/>
{/if}
<div
<div
bind:this={navElement}
bind:this={navElement}
id="sidebar"
id="sidebar"
class="h-screen max-h-[100dvh] min-h-screen {$showSidebar
class="h-screen max-h-[100dvh] min-h-screen
select-none
{$showSidebar
? '
lg
:relative w-[260px]'
? '
md
:relative w-[260px]'
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 rounded-r-2xl
: '-translate-x-[260px] w-[0px]'} bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200 text-sm transition fixed z-50 top-0 left-0 rounded-r-2xl
"
"
data-state={$showSidebar}
data-state={$showSidebar}
>
>
<div
<div
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] {$showSidebar
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px]
z-50
{$showSidebar
? ''
? ''
: 'invisible'}"
: 'invisible'}"
>
>
<div class="px-2 flex justify-
center
space-x-
2
">
<div class="px-2
.5
flex justify-
between
space-x-
1 text-gray-600 dark:text-gray-400
">
<a
<a
id="sidebar-new-chat-button"
id="sidebar-new-chat-button"
class="flex
-grow
flex justify-between rounded-xl px-
4
py-2 hover:bg-gray-100 dark:hover:bg-gray-
90
0 transition"
class="flex flex
-1
justify-between rounded-xl px-
2
py-2 hover:bg-gray-100 dark:hover:bg-gray-
85
0 transition"
href="/"
href="/"
draggable="false"
on:click={async () => {
on:click={async () => {
selectedChatId = null;
selectedChatId = null;
...
@@ -208,27 +239,30 @@
...
@@ -208,27 +239,30 @@
const newChatButton = document.getElementById('new-chat-button');
const newChatButton = document.getElementById('new-chat-button');
setTimeout(() => {
setTimeout(() => {
newChatButton?.click();
newChatButton?.click();
if ($mobile) {
showSidebar.set(false);
}
}, 0);
}, 0);
}}
}}
>
>
<div class="flex self-center">
<div class="self-center mx-1.5">
<div class="self-center mr-1.5">
<img
<img
crossorigin="anonymous"
src="{WEBUI_BASE_URL}/static/favicon.png"
src="{WEBUI_BASE_URL}/static/favicon.png"
class=" size-6 -translate-x-1.5 rounded-full"
class=" size-6 -translate-x-1.5 rounded-full"
alt="logo"
alt="logo"
/>
/>
</div>
<div class=" self-center font-medium text-sm">{$i18n.t('New Chat')}</div>
</div>
</div>
<div class=" self-center font-medium text-sm text-gray-850 dark:text-white">
<div class="self-center">
{$i18n.t('New Chat')}
</div>
<div class="self-center ml-auto">
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
viewBox="0 0 20 20"
fill="currentColor"
fill="currentColor"
class="
w-4 h-4
"
class="
size-5
"
>
>
<path
<path
d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"
...
@@ -239,17 +273,42 @@
...
@@ -239,17 +273,42 @@
</svg>
</svg>
</div>
</div>
</a>
</a>
<button
class=" cursor-pointer px-2 py-2 flex rounded-xl hover:bg-gray-100 dark:hover:bg-gray-850 transition"
on:click={() => {
showSidebar.set(!$showSidebar);
}}
>
<div class=" m-auto self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="size-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25H12"
/>
</svg>
</div>
</button>
</div>
</div>
{#if $user?.role === 'admin'}
{#if $user?.role === 'admin'}
<div class="px-2 flex justify-center
mt-0.5
">
<div class="px-2
.5
flex justify-center
text-gray-800 dark:text-gray-200
">
<a
<a
class="flex-grow flex space-x-3 rounded-xl px-
3
.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
class="flex-grow flex space-x-3 rounded-xl px-
2
.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/
modelfiles
"
href="/
workspace
"
on:click={() => {
on:click={() => {
selectedChatId = null;
selectedChatId = null;
chatId.set('');
chatId.set('');
}}
}}
draggable="false"
>
>
<div class="self-center">
<div class="self-center">
<svg
<svg
...
@@ -258,7 +317,7 @@
...
@@ -258,7 +317,7 @@
viewBox="0 0 24 24"
viewBox="0 0 24 24"
stroke-width="2"
stroke-width="2"
stroke="currentColor"
stroke="currentColor"
class="
w-4 h-4
"
class="
size-[1.1rem]
"
>
>
<path
<path
stroke-linecap="round"
stroke-linecap="round"
...
@@ -269,71 +328,7 @@
...
@@ -269,71 +328,7 @@
</div>
</div>
<div class="flex self-center">
<div class="flex self-center">
<div class=" self-center font-medium text-sm">{$i18n.t('Modelfiles')}</div>
<div class=" self-center font-medium text-sm">{$i18n.t('Workspace')}</div>
</div>
</a>
</div>
<div class="px-2 flex justify-center">
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/prompts"
on:click={() => {
selectedChatId = null;
chatId.set('');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
/>
</svg>
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">{$i18n.t('Prompts')}</div>
</div>
</a>
</div>
<div class="px-2 flex justify-center mb-1">
<a
class="flex-grow flex space-x-3 rounded-xl px-3.5 py-2 hover:bg-gray-100 dark:hover:bg-gray-900 transition"
href="/documents"
on:click={() => {
selectedChatId = null;
chatId.set('');
}}
>
<div class="self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="w-4 h-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
/>
</svg>
</div>
<div class="flex self-center">
<div class=" self-center font-medium text-sm">{$i18n.t('Documents')}</div>
</div>
</div>
</a>
</a>
</div>
</div>
...
@@ -383,9 +378,9 @@
...
@@ -383,9 +378,9 @@
</div>
</div>
{/if}
{/if}
<div class="px-2 mt-
1
mb-2 flex justify-center space-x-2">
<div class="px-2 mt-
0.5
mb-2 flex justify-center space-x-2">
<div class="flex w-full" id="chat-search">
<div class="flex w-full
rounded-xl
" id="chat-search">
<div class="self-center pl-3 py-2 rounded-l-xl bg-
white dark:bg-gray-950
">
<div class="self-center pl-3 py-2 rounded-l-xl bg-
transparent
">
<svg
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
viewBox="0 0 20 20"
...
@@ -401,7 +396,7 @@
...
@@ -401,7 +396,7 @@
</div>
</div>
<input
<input
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm dark:text-gray-300
dark:bg-gray-950
outline-none"
class="w-full rounded-r-xl py-1.5 pl-2.5 pr-4 text-sm
bg-transparent
dark:text-gray-300 outline-none"
placeholder={$i18n.t('Search')}
placeholder={$i18n.t('Search')}
bind:value={search}
bind:value={search}
on:focus={() => {
on:focus={() => {
...
@@ -412,9 +407,9 @@
...
@@ -412,9 +407,9 @@
</div>
</div>
{#if $tags.length > 0}
{#if $tags.length > 0}
<div class="px-2.5
mt-0.5
mb-2 flex gap-1 flex-wrap">
<div class="px-2.5 mb-2 flex gap-1 flex-wrap">
<button
<button
class="px-2.5 text-xs font-medium bg-gray-
10
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-
5
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
on:click={async () => {
on:click={async () => {
await chats.set(await getChatList(localStorage.token));
await chats.set(await getChatList(localStorage.token));
}}
}}
...
@@ -423,7 +418,7 @@
...
@@ -423,7 +418,7 @@
</button>
</button>
{#each $tags as tag}
{#each $tags as tag}
<button
<button
class="px-2.5 text-xs font-medium bg-gray-
10
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
class="px-2.5 text-xs font-medium bg-gray-
5
0 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
on:click={async () => {
on:click={async () => {
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
let chatIds = await getChatListByTagName(localStorage.token, tag.name);
if (chatIds.length === 0) {
if (chatIds.length === 0) {
...
@@ -439,7 +434,7 @@
...
@@ -439,7 +434,7 @@
</div>
</div>
{/if}
{/if}
<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-
none
">
<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-
hidden
">
{#each filteredChatList as chat, idx}
{#each filteredChatList as chat, idx}
{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
<div
<div
...
@@ -494,7 +489,7 @@
...
@@ -494,7 +489,7 @@
href="/c/{chat.id}"
href="/c/{chat.id}"
on:click={() => {
on:click={() => {
selectedChatId = chat.id;
selectedChatId = chat.id;
if (
window.innerWidth < 1024
) {
if (
$mobile
) {
showSidebar.set(false);
showSidebar.set(false);
}
}
}}
}}
...
@@ -609,6 +604,9 @@
...
@@ -609,6 +604,9 @@
shareChatId = selectedChatId;
shareChatId = selectedChatId;
showShareChatModal = true;
showShareChatModal = true;
}}
}}
archiveChatHandler={() => {
archiveChatHandler(chat.id);
}}
renameHandler={() => {
renameHandler={() => {
chatTitle = chat.title;
chatTitle = chat.title;
chatTitleEditId = chat.id;
chatTitleEditId = chat.id;
...
@@ -640,18 +638,6 @@
...
@@ -640,18 +638,6 @@
</button>
</button>
</ChatMenu>
</ChatMenu>
<Tooltip content={$i18n.t('Archive')}>
<button
aria-label="Archive"
class=" self-center dark:hover:text-white transition"
on:click={() => {
archiveChatHandler(chat.id);
}}
>
<ArchiveBox />
</button>
</Tooltip>
{#if chat.id === $chatId}
{#if chat.id === $chatId}
<button
<button
id="delete-chat-button"
id="delete-chat-button"
...
@@ -685,171 +671,38 @@
...
@@ -685,171 +671,38 @@
<div class="flex flex-col">
<div class="flex flex-col">
{#if $user !== undefined}
{#if $user !== undefined}
<button
<UserMenu
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
role={$user.role}
on:click={() => {
on:show={(e) => {
showDropdown = !showDropdown;
if (e.detail === 'archived-chat') {
showArchivedChats.set(true);
}
}}
}}
>
>
<div class=" self-center mr-3">
<button
<img
class=" flex rounded-xl py-3 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
src={$user.profile_image_url}
on:click={() => {
class=" max-w-[30px] object-cover rounded-full"
showDropdown = !showDropdown;
alt="User profile"
}}
/>
</div>
<div class=" self-center font-semibold">{$user.name}</div>
</button>
{#if showDropdown}
<div
id="dropdownDots"
class="absolute z-40 bottom-[70px] rounded-lg shadow w-[240px] bg-white dark:bg-gray-900"
transition:fade|slide={{ duration: 100 }}
>
>
<div class="p-1 py-2 w-full">
<div class=" self-center mr-3">
{#if $user.role === 'admin'}
<img
<button
src={$user.profile_image_url}
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
class=" max-w-[30px] object-cover rounded-full"
on:click={() => {
alt="User profile"
goto('/admin');
/>
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
</button>
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
goto('/playground');
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m6.75 7.5 3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0 0 21 18V6a2.25 2.25 0 0 0-2.25-2.25H5.25A2.25 2.25 0 0 0 3 6v12a2.25 2.25 0 0 0 2.25 2.25Z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Playground')}</div>
</button>
{/if}
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
showArchivedChatsModal = true;
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<ArchiveBox className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
</button>
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={async () => {
await showSettings.set(true);
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
</button>
</div>
<hr class=" dark:border-gray-800 m-0 p-0" />
<div class="p-1 py-2 w-full">
<button
class="flex rounded-md py-2.5 px-3.5 w-full hover:bg-gray-100 dark:hover:bg-gray-800 transition"
on:click={() => {
localStorage.removeItem('token');
location.href = '/auth';
showDropdown = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
</button>
</div>
</div>
</div>
<div class=" self-center font-semibold">{$user.name}</div>
{/if}
</button>
</UserMenu>
{/if}
{/if}
</div>
</div>
</div>
</div>
</div>
</div>
<div
<!--
<div
id="sidebar-handle"
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="
hidden md:
fixed left-0 top-[50dvh] -translate-y-1/2 transition-transform translate-x-[255px] md:translate-x-[260px] rotate-0"
>
>
<Tooltip
<Tooltip
placement="right"
placement="right"
...
@@ -882,16 +735,16 @@
...
@@ -882,16 +735,16 @@
</span>
</span>
</button>
</button>
</Tooltip>
</Tooltip>
</div>
</div>
-->
</div>
</div>
<style>
<style>
.scrollbar-
none
:active::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:active::-webkit-scrollbar-thumb,
.scrollbar-
none
:focus::-webkit-scrollbar-thumb,
.scrollbar-
hidden
:focus::-webkit-scrollbar-thumb,
.scrollbar-
none
:hover::-webkit-scrollbar-thumb {
.scrollbar-
hidden
:hover::-webkit-scrollbar-thumb {
visibility: visible;
visibility: visible;
}
}
.scrollbar-
none
::-webkit-scrollbar-thumb {
.scrollbar-
hidden
::-webkit-scrollbar-thumb {
visibility: hidden;
visibility: hidden;
}
}
</style>
</style>
src/lib/components/layout/Sidebar/ChatMenu.svelte
View file @
be5534c6
...
@@ -9,10 +9,12 @@
...
@@ -9,10 +9,12 @@
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tooltip from '$lib/components/common/Tooltip.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Tags from '$lib/components/chat/Tags.svelte';
import Share from '$lib/components/icons/Share.svelte';
import Share from '$lib/components/icons/Share.svelte';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
const i18n = getContext('i18n');
const i18n = getContext('i18n');
export let shareHandler: Function;
export let shareHandler: Function;
export let archiveChatHandler: Function;
export let renameHandler: Function;
export let renameHandler: Function;
export let deleteHandler: Function;
export let deleteHandler: Function;
export let onClose: Function;
export let onClose: Function;
...
@@ -36,14 +38,14 @@
...
@@ -36,14 +38,14 @@
<div slot="content">
<div slot="content">
<DropdownMenu.Content
<DropdownMenu.Content
class="w-full max-w-[1
8
0px] rounded-l
g
px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-
90
0 dark:text-white shadow"
class="w-full max-w-[1
6
0px] rounded-
x
l px-1 py-1.5 border border-gray-300/30 dark:border-gray-700/50 z-50 bg-white dark:bg-gray-
85
0 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 dark:hover:bg-gray-8
5
0 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer
hover:bg-gray-50
dark:hover:bg-gray-80
0
rounded-md"
on:click={() => {
on:click={() => {
shareHandler();
shareHandler();
}}
}}
...
@@ -53,7 +55,7 @@
...
@@ -53,7 +55,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 dark:hover:bg-gray-8
5
0 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer
hover:bg-gray-50
dark:hover:bg-gray-8
0
0 rounded-md"
on:click={() => {
on:click={() => {
renameHandler();
renameHandler();
}}
}}
...
@@ -63,7 +65,17 @@
...
@@ -63,7 +65,17 @@
</DropdownMenu.Item>
</DropdownMenu.Item>
<DropdownMenu.Item
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer dark:hover:bg-gray-850 rounded-md"
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
archiveChatHandler();
}}
>
<ArchiveBox strokeWidth="2" />
<div class="flex items-center">{$i18n.t('Archive')}</div>
</DropdownMenu.Item>
<DropdownMenu.Item
class="flex gap-2 items-center px-3 py-2 text-sm font-medium cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-md"
on:click={() => {
on:click={() => {
deleteHandler();
deleteHandler();
}}
}}
...
...
src/lib/components/layout/Sidebar/UserMenu.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
import { DropdownMenu } from 'bits-ui';
import { createEventDispatcher, getContext } from 'svelte';
import { flyAndScale } from '$lib/utils/transitions';
import { goto } from '$app/navigation';
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
import { showSettings } from '$lib/stores';
import { fade, slide } from 'svelte/transition';
const i18n = getContext('i18n');
export let show = false;
export let role = '';
export let className = 'max-w-[240px]';
const dispatch = createEventDispatcher();
</script>
<DropdownMenu.Root
bind:open={show}
onOpenChange={(state) => {
dispatch('change', state);
}}
>
<DropdownMenu.Trigger>
<slot />
</DropdownMenu.Trigger>
<slot name="content">
<DropdownMenu.Content
class="w-full {className} text-sm 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"
sideOffset={8}
side="bottom"
align="start"
transition={(e) => fade(e, { duration: 100 })}
>
<button
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={async () => {
await showSettings.set(true);
show = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.107-1.204l-.527-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Settings')}</div>
</button>
<button
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={() => {
dispatch('show', 'archived-chat');
show = false;
}}
>
<div class=" self-center mr-3">
<ArchiveBox className="size-5" strokeWidth="1.5" />
</div>
<div class=" self-center font-medium">{$i18n.t('Archived Chats')}</div>
</button>
{#if role === 'admin'}
<button
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={() => {
goto('/admin');
show = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-5 h-5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M17.982 18.725A7.488 7.488 0 0012 15.75a7.488 7.488 0 00-5.982 2.975m11.963 0a9 9 0 10-11.963 0m11.963 0A8.966 8.966 0 0112 21a8.966 8.966 0 01-5.982-2.275M15 9.75a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Admin Panel')}</div>
</button>
{/if}
<hr class=" dark:border-gray-800 my-2 p-0" />
<button
class="flex rounded-md py-2 px-3 w-full hover:bg-gray-50 dark:hover:bg-gray-800 transition"
on:click={() => {
localStorage.removeItem('token');
location.href = '/auth';
show = false;
}}
>
<div class=" self-center mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-5 h-5"
>
<path
fill-rule="evenodd"
d="M3 4.25A2.25 2.25 0 015.25 2h5.5A2.25 2.25 0 0113 4.25v2a.75.75 0 01-1.5 0v-2a.75.75 0 00-.75-.75h-5.5a.75.75 0 00-.75.75v11.5c0 .414.336.75.75.75h5.5a.75.75 0 00.75-.75v-2a.75.75 0 011.5 0v2A2.25 2.25 0 0110.75 18h-5.5A2.25 2.25 0 013 15.75V4.25z"
clip-rule="evenodd"
/>
<path
fill-rule="evenodd"
d="M6 10a.75.75 0 01.75-.75h9.546l-1.048-.943a.75.75 0 111.004-1.114l2.5 2.25a.75.75 0 010 1.114l-2.5 2.25a.75.75 0 11-1.004-1.114l1.048-.943H6.75A.75.75 0 016 10z"
clip-rule="evenodd"
/>
</svg>
</div>
<div class=" self-center font-medium">{$i18n.t('Sign Out')}</div>
</button>
<!-- <DropdownMenu.Item class="flex items-center px-3 py-2 text-sm font-medium">
<div class="flex items-center">Profile</div>
</DropdownMenu.Item> -->
</DropdownMenu.Content>
</slot>
</DropdownMenu.Root>
src/lib/components/workspace/Documents.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, documents } from '$lib/stores';
import { createNewDoc, deleteDocByName, getDocs } from '$lib/apis/documents';
import { SUPPORTED_FILE_TYPE, SUPPORTED_FILE_EXTENSIONS } from '$lib/constants';
import { uploadDocToVectorDB } from '$lib/apis/rag';
import { transformFileName } from '$lib/utils';
import Checkbox from '$lib/components/common/Checkbox.svelte';
import EditDocModal from '$lib/components/documents/EditDocModal.svelte';
import AddFilesPlaceholder from '$lib/components/AddFilesPlaceholder.svelte';
import SettingsModal from '$lib/components/documents/SettingsModal.svelte';
import AddDocModal from '$lib/components/documents/AddDocModal.svelte';
const i18n = getContext('i18n');
let importFiles = '';
let inputFiles = '';
let query = '';
let documentsImportInputElement: HTMLInputElement;
let tags = [];
let showSettingsModal = false;
let showAddDocModal = false;
let showEditDocModal = false;
let selectedDoc;
let selectedTag = '';
let dragged = false;
const deleteDoc = async (name) => {
await deleteDocByName(localStorage.token, name);
await documents.set(await getDocs(localStorage.token));
};
const deleteDocs = async (docs) => {
const res = await Promise.all(
docs.map(async (doc) => {
return await deleteDocByName(localStorage.token, doc.name);
})
);
await documents.set(await getDocs(localStorage.token));
};
const uploadDoc = async (file) => {
const res = await uploadDocToVectorDB(localStorage.token, '', file).catch((error) => {
toast.error(error);
return null;
});
if (res) {
await createNewDoc(
localStorage.token,
res.collection_name,
res.filename,
transformFileName(res.filename),
res.filename
).catch((error) => {
toast.error(error);
return null;
});
await documents.set(await getDocs(localStorage.token));
}
};
onMount(() => {
documents.subscribe((docs) => {
tags = docs.reduce((a, e, i, arr) => {
return [...new Set([...a, ...(e?.content?.tags ?? []).map((tag) => tag.name)])];
}, []);
});
const dropZone = document.querySelector('body');
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDragLeave = () => {
dragged = false;
};
const onDrop = async (e) => {
e.preventDefault();
if (e.dataTransfer?.files) {
let reader = new FileReader();
reader.onload = (event) => {
files = [
...files,
{
type: 'image',
url: `${event.target.result}`
}
];
};
const inputFiles = e.dataTransfer?.files;
if (inputFiles && inputFiles.length > 0) {
for (const file of inputFiles) {
console.log(file, file.name.split('.').at(-1));
if (
SUPPORTED_FILE_TYPE.includes(file['type']) ||
SUPPORTED_FILE_EXTENSIONS.includes(file.name.split('.').at(-1))
) {
uploadDoc(file);
} else {
toast.error(
`Unknown File Type '${file['type']}', but accepting and treating as plain text`
);
uploadDoc(file);
}
}
} else {
toast.error($i18n.t(`File not found.`));
}
}
dragged = false;
};
dropZone?.addEventListener('dragover', onDragOver);
dropZone?.addEventListener('drop', onDrop);
dropZone?.addEventListener('dragleave', onDragLeave);
return () => {
dropZone?.removeEventListener('dragover', onDragOver);
dropZone?.removeEventListener('drop', onDrop);
dropZone?.removeEventListener('dragleave', onDragLeave);
};
});
let filteredDocs;
$: filteredDocs = $documents.filter(
(doc) =>
(selectedTag === '' ||
(doc?.content?.tags ?? []).map((tag) => tag.name).includes(selectedTag)) &&
(query === '' || doc.name.includes(query))
);
</script>
<svelte:head>
<title>
{$i18n.t('Documents')} | {$WEBUI_NAME}
</title>
</svelte:head>
{#if dragged}
<div
class="fixed w-full h-full flex z-50 touch-none pointer-events-none"
id="dropzone"
role="region"
aria-label="Drag and Drop Container"
>
<div class="absolute rounded-xl w-full h-full backdrop-blur bg-gray-800/40 flex justify-center">
<div class="m-auto pt-64 flex flex-col justify-center">
<div class="max-w-md">
<AddFilesPlaceholder>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents
</div>
</AddFilesPlaceholder>
</div>
</div>
</div>
</div>
{/if}
{#key selectedDoc}
<EditDocModal bind:show={showEditDocModal} {selectedDoc} />
{/key}
<AddDocModal bind:show={showAddDocModal} />
<SettingsModal bind:show={showSettingsModal} />
<div class="mb-3">
<div class="flex justify-between items-center">
<div class=" text-lg font-semibold self-center">{$i18n.t('Documents')}</div>
<div>
<button
class="flex items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition"
type="button"
on:click={() => {
showSettingsModal = !showSettingsModal;
}}
>
<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="M6.955 1.45A.5.5 0 0 1 7.452 1h1.096a.5.5 0 0 1 .497.45l.17 1.699c.484.12.94.312 1.356.562l1.321-1.081a.5.5 0 0 1 .67.033l.774.775a.5.5 0 0 1 .034.67l-1.08 1.32c.25.417.44.873.561 1.357l1.699.17a.5.5 0 0 1 .45.497v1.096a.5.5 0 0 1-.45.497l-1.699.17c-.12.484-.312.94-.562 1.356l1.082 1.322a.5.5 0 0 1-.034.67l-.774.774a.5.5 0 0 1-.67.033l-1.322-1.08c-.416.25-.872.44-1.356.561l-.17 1.699a.5.5 0 0 1-.497.45H7.452a.5.5 0 0 1-.497-.45l-.17-1.699a4.973 4.973 0 0 1-1.356-.562L4.108 13.37a.5.5 0 0 1-.67-.033l-.774-.775a.5.5 0 0 1-.034-.67l1.08-1.32a4.971 4.971 0 0 1-.561-1.357l-1.699-.17A.5.5 0 0 1 1 8.548V7.452a.5.5 0 0 1 .45-.497l1.699-.17c.12-.484.312-.94.562-1.356L2.629 4.107a.5.5 0 0 1 .034-.67l.774-.774a.5.5 0 0 1 .67-.033L5.43 3.71a4.97 4.97 0 0 1 1.356-.561l.17-1.699ZM6 8c0 .538.212 1.026.558 1.385l.057.057a2 2 0 0 0 2.828-2.828l-.058-.056A2 2 0 0 0 6 8Z"
clip-rule="evenodd"
/>
</svg>
<div class=" text-xs">{$i18n.t('Document Settings')}</div>
</button>
</div>
</div>
<div class=" text-gray-500 text-xs mt-1">
ⓘ {$i18n.t("Use '#' in the prompt input to load and select your documents.")}
</div>
</div>
<div class=" flex w-full space-x-2">
<div class="flex flex-1">
<div class=" self-center ml-1 mr-3">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="w-4 h-4"
>
<path
fill-rule="evenodd"
d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z"
clip-rule="evenodd"
/>
</svg>
</div>
<input
class=" w-full text-sm pr-4 py-1 rounded-r-xl outline-none bg-transparent"
bind:value={query}
placeholder={$i18n.t('Search Documents')}
/>
</div>
<div>
<button
class=" px-2 py-2 rounded-xl border border-gray-200 dark:border-gray-600 dark:border-0 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 transition font-medium text-sm flex items-center space-x-1"
on:click={() => {
showAddDocModal = true;
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 3.75a.75.75 0 0 0-1.5 0v3.5h-3.5a.75.75 0 0 0 0 1.5h3.5v3.5a.75.75 0 0 0 1.5 0v-3.5h3.5a.75.75 0 0 0 0-1.5h-3.5v-3.5Z"
/>
</svg>
</button>
</div>
</div>
<!-- <div>
<div
class="my-3 py-16 rounded-lg border-2 border-dashed dark:border-gray-600 {dragged &&
' dark:bg-gray-700'} "
role="region"
on:drop={onDrop}
on:dragover={onDragOver}
on:dragleave={onDragLeave}
>
<div class=" pointer-events-none">
<div class="text-center dark:text-white text-2xl font-semibold z-50">{$i18n.t('Add Files')}</div>
<div class=" mt-2 text-center text-sm dark:text-gray-200 w-full">
Drop any files here to add to my documents
</div>
</div>
</div>
</div> -->
<hr class=" dark:border-gray-850 my-2.5" />
{#if tags.length > 0}
<div class="px-2.5 pt-1 flex gap-1 flex-wrap">
<div class="ml-0.5 pr-3 my-auto flex items-center">
<Checkbox
state={filteredDocs.filter((doc) => doc?.selected === 'checked').length ===
filteredDocs.length
? 'checked'
: 'unchecked'}
indeterminate={filteredDocs.filter((doc) => doc?.selected === 'checked').length > 0 &&
filteredDocs.filter((doc) => doc?.selected === 'checked').length !== filteredDocs.length}
on:change={(e) => {
if (e.detail === 'checked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'checked' }));
} else if (e.detail === 'unchecked') {
filteredDocs = filteredDocs.map((doc) => ({ ...doc, selected: 'unchecked' }));
}
}}
/>
</div>
{#if filteredDocs.filter((doc) => doc?.selected === 'checked').length === 0}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">{$i18n.t('all')}</div>
</button>
{#each tags as tag}
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = tag;
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
#{tag}
</div>
</button>
{/each}
{:else}
<div class="flex-1 flex w-full justify-between items-center">
<div class="text-xs font-medium py-0.5 self-center mr-1">
{filteredDocs.filter((doc) => doc?.selected === 'checked').length} Selected
</div>
<div class="flex gap-1">
<!-- <button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
selectedTag = '';
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">add tags</div>
</button> -->
<button
class="px-2 py-0.5 space-x-1 flex h-fit items-center rounded-full transition bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:text-white"
on:click={async () => {
deleteDocs(filteredDocs.filter((doc) => doc.selected === 'checked'));
// await chats.set(await getChatListByTagName(localStorage.token, tag.name));
}}
>
<div class=" text-xs font-medium self-center line-clamp-1">
{$i18n.t('delete')}
</div>
</button>
</div>
</div>
{/if}
</div>
{/if}
<div class="my-3 mb-5">
{#each filteredDocs as doc}
<button
class=" flex space-x-4 cursor-pointer text-left w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
on:click={() => {
if (doc?.selected === 'checked') {
doc.selected = 'unchecked';
} else {
doc.selected = 'checked';
}
}}
>
<div class="my-auto flex items-center">
<Checkbox state={doc?.selected ?? 'unchecked'} />
</div>
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
<div class=" flex items-center space-x-3">
<div class="p-2.5 bg-red-400 text-white rounded-lg">
{#if doc}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="w-6 h-6"
>
<path
fill-rule="evenodd"
d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z"
clip-rule="evenodd"
/>
<path
d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z"
/>
</svg>
{:else}
<svg
class=" w-6 h-6 translate-y-[0.5px]"
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
><style>
.spinner_qM83 {
animation: spinner_8HQG 1.05s infinite;
}
.spinner_oXPr {
animation-delay: 0.1s;
}
.spinner_ZTLf {
animation-delay: 0.2s;
}
@keyframes spinner_8HQG {
0%,
57.14% {
animation-timing-function: cubic-bezier(0.33, 0.66, 0.66, 1);
transform: translate(0);
}
28.57% {
animation-timing-function: cubic-bezier(0.33, 0, 0.66, 0.33);
transform: translateY(-6px);
}
100% {
transform: translate(0);
}
}
</style><circle class="spinner_qM83" cx="4" cy="12" r="2.5" /><circle
class="spinner_qM83 spinner_oXPr"
cx="12"
cy="12"
r="2.5"
/><circle class="spinner_qM83 spinner_ZTLf" cx="20" cy="12" r="2.5" /></svg
>
{/if}
</div>
<div class=" self-center flex-1">
<div class=" font-bold line-clamp-1">#{doc.name} ({doc.filename})</div>
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
{doc.title}
</div>
</div>
</div>
</div>
<div class="flex flex-row space-x-1 self-center">
<button
class="self-center w-fit text-sm z-20 px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={async (e) => {
e.stopPropagation();
showEditDocModal = !showEditDocModal;
selectedDoc = doc;
}}
>
<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="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.863 4.487zm0 0L19.5 7.125"
/>
</svg>
</button>
<!-- <button
class="self-center w-fit text-sm px-2 py-2 border dark:border-gray-600 rounded-xl"
type="button"
on:click={() => {
console.log('download file');
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-4 h-4"
>
<path
d="M8.75 2.75a.75.75 0 0 0-1.5 0v5.69L5.03 6.22a.75.75 0 0 0-1.06 1.06l3.5 3.5a.75.75 0 0 0 1.06 0l3.5-3.5a.75.75 0 0 0-1.06-1.06L8.75 8.44V2.75Z"
/>
<path
d="M3.5 9.75a.75.75 0 0 0-1.5 0v1.5A2.75 2.75 0 0 0 4.75 14h6.5A2.75 2.75 0 0 0 14 11.25v-1.5a.75.75 0 0 0-1.5 0v1.5c0 .69-.56 1.25-1.25 1.25h-6.5c-.69 0-1.25-.56-1.25-1.25v-1.5Z"
/>
</svg>
</button> -->
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={(e) => {
e.stopPropagation();
deleteDoc(doc.name);
}}
>
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</button>
</div>
</button>
{/each}
</div>
<div class=" flex justify-end w-full mb-2">
<div class="flex space-x-2">
<input
id="documents-import-input"
bind:this={documentsImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
const reader = new FileReader();
reader.onload = async (event) => {
const savedDocs = JSON.parse(event.target.result);
console.log(savedDocs);
for (const doc of savedDocs) {
await createNewDoc(
localStorage.token,
doc.collection_name,
doc.filename,
doc.name,
doc.title
).catch((error) => {
toast.error(error);
return null;
});
}
await documents.set(await getDocs(localStorage.token));
};
reader.readAsText(importFiles[0]);
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
documentsImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Documents Mapping')}</div>
<div class=" self-center">
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
let blob = new Blob([JSON.stringify($documents)], {
type: 'application/json'
});
saveAs(blob, `documents-mapping-export-${Date.now()}.json`);
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Documents Mapping')}</div>
<div class=" self-center">
<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="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
</div>
src/lib/components/workspace/Modelfiles.svelte
0 → 100644
View file @
be5534c6
<script lang="ts">
import { toast } from 'svelte-sonner';
import fileSaver from 'file-saver';
const { saveAs } = fileSaver;
import { onMount, getContext } from 'svelte';
import { WEBUI_NAME, modelfiles, settings, user } from '$lib/stores';
import { createModel, deleteModel } from '$lib/apis/ollama';
import {
createNewModelfile,
deleteModelfileByTagName,
getModelfiles
} from '$lib/apis/modelfiles';
import { goto } from '$app/navigation';
const i18n = getContext('i18n');
let localModelfiles = [];
let importFiles;
let modelfilesImportInputElement: HTMLInputElement;
const deleteModelHandler = async (tagName) => {
let success = null;
success = await deleteModel(localStorage.token, tagName).catch((err) => {
toast.error(err);
return null;
});
if (success) {
toast.success($i18n.t(`Deleted {{tagName}}`, { tagName }));
}
return success;
};
const deleteModelfile = async (tagName) => {
await deleteModelHandler(tagName);
await deleteModelfileByTagName(localStorage.token, tagName);
await modelfiles.set(await getModelfiles(localStorage.token));
};
const shareModelfile = async (modelfile) => {
toast.success($i18n.t('Redirecting you to OpenWebUI Community'));
const url = 'https://openwebui.com';
const tab = await window.open(`${url}/modelfiles/create`, '_blank');
window.addEventListener(
'message',
(event) => {
if (event.origin !== url) return;
if (event.data === 'loaded') {
tab.postMessage(JSON.stringify(modelfile), '*');
}
},
false
);
};
const saveModelfiles = async (modelfiles) => {
let blob = new Blob([JSON.stringify(modelfiles)], {
type: 'application/json'
});
saveAs(blob, `modelfiles-export-${Date.now()}.json`);
};
onMount(() => {
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
if (localModelfiles) {
console.log(localModelfiles);
}
});
</script>
<svelte:head>
<title>
{$i18n.t('Modelfiles')} | {$WEBUI_NAME}
</title>
</svelte:head>
<div class=" text-lg font-semibold mb-3">{$i18n.t('Modelfiles')}</div>
<a class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2" href="/workspace/modelfiles/create">
<div class=" self-center w-10">
<div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
<path
fill-rule="evenodd"
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
<div class=" self-center">
<div class=" font-bold">{$i18n.t('Create a modelfile')}</div>
<div class=" text-sm">{$i18n.t('Customize Ollama models for a specific purpose')}</div>
</div>
</a>
<hr class=" dark:border-gray-850" />
<div class=" my-2 mb-5">
{#each $modelfiles as modelfile}
<div
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
>
<a
class=" flex flex-1 space-x-4 cursor-pointer w-full"
href={`/?models=${encodeURIComponent(modelfile.tagName)}`}
>
<div class=" self-center w-10">
<div class=" rounded-full bg-stone-700">
<img
src={modelfile.imageUrl ?? '/user.png'}
alt="modelfile profile"
class=" rounded-full w-full h-auto object-cover"
/>
</div>
</div>
<div class=" flex-1 self-center">
<div class=" font-bold capitalize">{modelfile.title}</div>
<div class=" text-sm overflow-hidden text-ellipsis line-clamp-1">
{modelfile.desc}
</div>
</div>
</a>
<div class="flex flex-row space-x-1 self-center">
<a
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
href={`/workspace/modelfiles/edit?tag=${encodeURIComponent(modelfile.tagName)}`}
>
<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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125"
/>
</svg>
</a>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
// console.log(modelfile);
sessionStorage.modelfile = JSON.stringify(modelfile);
goto('/workspace/modelfiles/create');
}}
>
<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="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 0 1-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 0 1 1.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 0 0-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 0 1-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 0 0-3.375-3.375h-1.5a1.125 1.125 0 0 1-1.125-1.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H9.75"
/>
</svg>
</button>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
shareModelfile(modelfile);
}}
>
<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="M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z"
/>
</svg>
</button>
<button
class="self-center w-fit text-sm px-2 py-2 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
type="button"
on:click={() => {
deleteModelfile(modelfile.tagName);
}}
>
<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="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
</button>
</div>
</div>
{/each}
</div>
<div class=" flex justify-end w-full mb-3">
<div class="flex space-x-1">
<input
id="modelfiles-import-input"
bind:this={modelfilesImportInputElement}
bind:files={importFiles}
type="file"
accept=".json"
hidden
on:change={() => {
console.log(importFiles);
let reader = new FileReader();
reader.onload = async (event) => {
let savedModelfiles = JSON.parse(event.target.result);
console.log(savedModelfiles);
for (const modelfile of savedModelfiles) {
await createNewModelfile(localStorage.token, modelfile).catch((error) => {
return null;
});
}
await modelfiles.set(await getModelfiles(localStorage.token));
};
reader.readAsText(importFiles[0]);
}}
/>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={() => {
modelfilesImportInputElement.click();
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Import Modelfiles')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 9.5a.75.75 0 0 1-.75-.75V8.06l-.72.72a.75.75 0 0 1-1.06-1.06l2-2a.75.75 0 0 1 1.06 0l2 2a.75.75 0 1 1-1.06 1.06l-.72-.72v2.69a.75.75 0 0 1-.75.75Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="flex text-xs items-center space-x-1 px-3 py-1.5 rounded-xl bg-gray-50 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-gray-700 dark:text-gray-200 transition"
on:click={async () => {
saveModelfiles($modelfiles);
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Export Modelfiles')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M4 2a1.5 1.5 0 0 0-1.5 1.5v9A1.5 1.5 0 0 0 4 14h8a1.5 1.5 0 0 0 1.5-1.5V6.621a1.5 1.5 0 0 0-.44-1.06L9.94 2.439A1.5 1.5 0 0 0 8.878 2H4Zm4 3.5a.75.75 0 0 1 .75.75v2.69l.72-.72a.75.75 0 1 1 1.06 1.06l-2 2a.75.75 0 0 1-1.06 0l-2-2a.75.75 0 0 1 1.06-1.06l.72.72V6.25A.75.75 0 0 1 8 5.5Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
</div>
{#if localModelfiles.length > 0}
<div class="flex">
<div class=" self-center text-sm font-medium mr-4">
{localModelfiles.length} Local Modelfiles Detected
</div>
<div class="flex space-x-1">
<button
class="self-center w-fit text-sm px-3 py-1 border dark:border-gray-600 rounded-xl flex"
on:click={async () => {
for (const modelfile of localModelfiles) {
await createNewModelfile(localStorage.token, modelfile).catch((error) => {
return null;
});
}
saveModelfiles(localModelfiles);
localStorage.removeItem('modelfiles');
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
await modelfiles.set(await getModelfiles(localStorage.token));
}}
>
<div class=" self-center mr-2 font-medium">{$i18n.t('Sync All')}</div>
<div class=" self-center">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
class="w-3.5 h-3.5"
>
<path
fill-rule="evenodd"
d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.932.75.75 0 0 1-1.3-.75 6 6 0 0 1 9.44-1.242l.842.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44 1.241l-.84-.84v1.371a.75.75 0 0 1-1.5 0V9.591a.75.75 0 0 1 .75-.75H5.35a.75.75 0 0 1 0 1.5H3.98l.841.841a4.5 4.5 0 0 0 7.08-.932.75.75 0 0 1 1.025-.273Z"
clip-rule="evenodd"
/>
</svg>
</div>
</button>
<button
class="self-center w-fit text-sm p-1.5 border dark:border-gray-600 rounded-xl flex"
on:click={async () => {
saveModelfiles(localModelfiles);
localStorage.removeItem('modelfiles');
localModelfiles = JSON.parse(localStorage.getItem('modelfiles') ?? '[]');
await modelfiles.set(await getModelfiles(localStorage.token));
}}
>
<div class=" self-center">
<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="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0"
/>
</svg>
</div>
</button>
</div>
</div>
{/if}
</div>
<div class=" my-16">
<div class=" text-lg font-semibold mb-3">{$i18n.t('Made by OpenWebUI Community')}</div>
<a
class=" flex space-x-4 cursor-pointer w-full mb-2 px-3 py-2"
href="https://openwebui.com/"
target="_blank"
>
<div class=" self-center w-10">
<div
class="w-full h-10 flex justify-center rounded-full bg-transparent dark:bg-gray-700 border border-dashed border-gray-200"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="w-6">
<path
fill-rule="evenodd"
d="M12 3.75a.75.75 0 01.75.75v6.75h6.75a.75.75 0 010 1.5h-6.75v6.75a.75.75 0 01-1.5 0v-6.75H4.5a.75.75 0 010-1.5h6.75V4.5a.75.75 0 01.75-.75z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
<div class=" self-center">
<div class=" font-bold">{$i18n.t('Discover a modelfile')}</div>
<div class=" text-sm">{$i18n.t('Discover, download, and explore model presets')}</div>
</div>
</a>
</div>
Prev
1
2
3
4
5
6
7
8
Next
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment