This commit is contained in:
Timothy Jaeryang Baek
2026-04-14 17:22:54 -05:00
parent 8bd23b9145
commit ecd74f220c
9 changed files with 307 additions and 5 deletions
+63
View File
@@ -313,3 +313,66 @@ export const deleteNoteById = async (token: string, id: string) => {
return res;
};
export const getPinnedNoteList = async (token: string = '') => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/pinned`, {
method: 'GET',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
return res ?? [];
};
export const toggleNotePinnedStatusById = async (token: string, id: string) => {
let error = null;
const res = await fetch(`${WEBUI_API_BASE_URL}/notes/${id}/pin`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
authorization: `Bearer ${token}`
}
})
.then(async (res) => {
if (!res.ok) throw await res.json();
return res.json();
})
.then((json) => {
return json;
})
.catch((err) => {
error = err.detail;
console.error(err);
return null;
});
if (error) {
throw error;
}
return res;
};
+54
View File
@@ -16,6 +16,7 @@
mobile,
showArchivedChats,
pinnedChats,
pinnedNotes,
scrollPaginationEnabled,
currentChatPage,
temporaryChatEnabled,
@@ -44,6 +45,7 @@
} from '$lib/apis/chats';
import { createNewFolder, getFolders, updateFolderParentIdById } from '$lib/apis/folders';
import { checkActiveChats } from '$lib/apis/tasks';
import { getPinnedNoteList, toggleNotePinnedStatusById } from '$lib/apis/notes';
import { WEBUI_API_BASE_URL, WEBUI_BASE_URL } from '$lib/constants';
import ArchivedChatsModal from './ArchivedChatsModal.svelte';
@@ -86,6 +88,7 @@
let pinnedModels = [];
let showPinnedModels = false;
let showPinnedNotes = false;
let showChannels = false;
let showFolders = false;
@@ -227,6 +230,13 @@
const _pinnedChats = await getPinnedChatList(localStorage.token);
pinnedChats.set(_pinnedChats);
})(),
await (async () => {
if ($config?.features?.enable_notes && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true))) {
console.log('Init pinned notes');
const _pinnedNotes = await getPinnedNoteList(localStorage.token).catch(() => []);
pinnedNotes.set(_pinnedNotes);
}
})(),
await (async () => {
console.log('Init chat list');
const _chats = await getChatList(localStorage.token, $currentChatPage);
@@ -1072,6 +1082,50 @@
</Folder>
{/if}
{#if ($config?.features?.enable_notes ?? false) && ($user?.role === 'admin' || ($user?.permissions?.features?.notes ?? true)) && $pinnedNotes.length > 0}
<Folder
id="sidebar-pinned-notes"
bind:open={showPinnedNotes}
className="px-2 mt-0.5"
name={$i18n.t('Notes')}
chevron={false}
dragAndDrop={false}
>
<div class="mt-0.5 pb-1.5">
{#each $pinnedNotes as note (note.id)}
<a
class="w-full flex items-center gap-2.5 rounded-xl px-2.5 py-1.5 hover:bg-gray-100 dark:hover:bg-gray-900 transition group text-sm"
href={`/notes/${note.id}`}
on:click={() => {
itemClickHandler();
}}
draggable="false"
>
<div class="self-center">
<Note className="size-4" strokeWidth="2" />
</div>
<div class="flex-1 text-ellipsis line-clamp-1">
{note.title}
</div>
<button
class="invisible group-hover:visible self-center p-0.5 hover:bg-gray-200 dark:hover:bg-gray-800 rounded-lg transition"
on:click|preventDefault|stopPropagation={async () => {
await toggleNotePinnedStatusById(localStorage.token, note.id);
const _pinnedNotes = await getPinnedNoteList(localStorage.token).catch(() => []);
pinnedNotes.set(_pinnedNotes);
}}
aria-label={$i18n.t('Unpin')}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-3.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</a>
{/each}
</div>
</Folder>
{/if}
{#if $config?.features?.enable_channels && ($user?.role === 'admin' || ($user?.permissions?.features?.channels ?? true))}
<Folder
id="sidebar-channels"
+11 -2
View File
@@ -35,7 +35,8 @@
showSidebar,
socket,
user,
WEBUI_NAME
WEBUI_NAME,
pinnedNotes
} from '$lib/stores';
import { downloadPdf } from './utils';
@@ -64,7 +65,9 @@
deleteNoteById,
getNoteById,
updateNoteById,
updateNoteAccessGrants
updateNoteAccessGrants,
toggleNotePinnedStatusById,
getPinnedNoteList
} from '$lib/apis/notes';
import RichTextInput from '../common/RichTextInput.svelte';
@@ -1088,6 +1091,12 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
onDelete={() => {
showDeleteConfirm = true;
}}
isPinned={note.is_pinned ?? false}
onPin={async () => {
await toggleNotePinnedStatusById(localStorage.token, note.id);
note = await getNoteById(localStorage.token, note.id);
pinnedNotes.set(await getPinnedNoteList(localStorage.token).catch(() => []));
}}
>
<div class="p-1 bg-transparent hover:bg-white/5 transition rounded-lg">
<EllipsisHorizontal className="size-5" />
+16 -2
View File
@@ -30,13 +30,15 @@
$: loadLocale($i18n.languages);
import { goto } from '$app/navigation';
import { WEBUI_NAME, config, user } from '$lib/stores';
import { WEBUI_NAME, config, user, pinnedNotes } from '$lib/stores';
import {
createNewNote,
deleteNoteById,
getNoteById,
getNoteList,
searchNotes
searchNotes,
toggleNotePinnedStatusById,
getPinnedNoteList
} from '$lib/apis/notes';
import { capitalizeFirstLetter, copyToClipboard, getTimeRange } from '$lib/utils';
import { downloadPdf, createNoteHandler } from './utils';
@@ -540,6 +542,12 @@
selectedNote = note;
showDeleteConfirm = true;
}}
isPinned={note.is_pinned ?? false}
onPin={async () => {
await toggleNotePinnedStatusById(localStorage.token, note.id);
pinnedNotes.set(await getPinnedNoteList(localStorage.token).catch(() => []));
init();
}}
>
<button
class="self-center w-fit text-sm p-1 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
@@ -602,6 +610,12 @@
selectedNote = note;
showDeleteConfirm = true;
}}
isPinned={note.is_pinned ?? false}
onPin={async () => {
await toggleNotePinnedStatusById(localStorage.token, note.id);
pinnedNotes.set(await getPinnedNoteList(localStorage.token).catch(() => []));
init();
}}
>
<button
class="self-center w-fit text-sm p-1 dark:text-gray-300 dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/5 rounded-xl"
@@ -8,6 +8,8 @@
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
import Share from '$lib/components/icons/Share.svelte';
import Link from '$lib/components/icons/Link.svelte';
import Pin from '$lib/components/icons/Pin.svelte';
import PinSlash from '$lib/components/icons/PinSlash.svelte';
const i18n = getContext('i18n');
@@ -16,6 +18,8 @@
export let onDownload = (type) => {};
export let onDelete = () => {};
export let onPin = null;
export let isPinned = false;
export let onCopyLink = null;
export let onCopyToClipboard = null;
@@ -110,7 +114,25 @@
</DropdownSub>
{/if}
{#if onPin}
<button
class="select-none flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
on:click={() => {
onPin();
show = false;
}}
>
{#if isPinned}
<PinSlash />
<div class="flex items-center">{$i18n.t('Unpin')}</div>
{:else}
<Pin />
<div class="flex items-center">{$i18n.t('Pin to Sidebar')}</div>
{/if}
</button>
{/if}
<button
class="select-none flex gap-2 items-center px-3 py-1.5 text-sm cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800 rounded-xl w-full"
on:click={() => {
onDelete();
+1
View File
@@ -59,6 +59,7 @@ export const channelId = writable(null);
export const chats = writable(null);
export const pinnedChats = writable([]);
export const pinnedNotes = writable([]);
export const tags = writable([]);
export const folders = writable([]);