mirror of
https://github.com/open-webui/open-webui
synced 2025-02-20 12:00:22 +00:00
enh: pinned chats support
This commit is contained in:
parent
439ab7a335
commit
05ec71beb9
@ -8,7 +8,7 @@
|
||||
getTagsById,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { tags as _tags, chats } from '$lib/stores';
|
||||
import { tags as _tags, chats, pinnedChats } from '$lib/stores';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
@ -19,9 +19,11 @@
|
||||
let tags = [];
|
||||
|
||||
const getTags = async () => {
|
||||
return await getTagsById(localStorage.token, chatId).catch(async (error) => {
|
||||
return [];
|
||||
});
|
||||
return (
|
||||
await getTagsById(localStorage.token, chatId).catch(async (error) => {
|
||||
return [];
|
||||
})
|
||||
).filter((tag) => tag.name !== 'pinned');
|
||||
};
|
||||
|
||||
const addTag = async (tagName) => {
|
||||
@ -33,6 +35,7 @@
|
||||
});
|
||||
|
||||
_tags.set(await getAllChatTags(localStorage.token));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
};
|
||||
|
||||
const deleteTag = async (tagName) => {
|
||||
@ -44,19 +47,23 @@
|
||||
});
|
||||
|
||||
console.log($_tags);
|
||||
|
||||
await _tags.set(await getAllChatTags(localStorage.token));
|
||||
|
||||
console.log($_tags);
|
||||
|
||||
if ($_tags.map((t) => t.name).includes(tagName)) {
|
||||
await chats.set(await getChatListByTagName(localStorage.token, tagName));
|
||||
if (tagName === 'pinned') {
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
} else {
|
||||
await chats.set(await getChatListByTagName(localStorage.token, tagName));
|
||||
}
|
||||
|
||||
if ($chats.find((chat) => chat.id === chatId)) {
|
||||
dispatch('close');
|
||||
}
|
||||
} else {
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
}
|
||||
};
|
||||
|
||||
|
19
src/lib/components/icons/Bookmark.svelte
Normal file
19
src/lib/components/icons/Bookmark.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M17.593 3.322c1.1.128 1.907 1.077 1.907 2.185V21L12 17.25 4.5 21V5.507c0-1.108.806-2.057 1.907-2.185a48.507 48.507 0 0 1 11.186 0Z"
|
||||
/>
|
||||
</svg>
|
19
src/lib/components/icons/BookmarkSlash.svelte
Normal file
19
src/lib/components/icons/BookmarkSlash.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="m3 3 1.664 1.664M21 21l-1.5-1.5m-5.485-1.242L12 17.25 4.5 21V8.742m.164-4.078a2.15 2.15 0 0 1 1.743-1.342 48.507 48.507 0 0 1 11.186 0c1.1.128 1.907 1.077 1.907 2.185V19.5M4.664 4.664 19.5 19.5"
|
||||
/>
|
||||
</svg>
|
124
src/lib/components/icons/ChatMenu.svelte
Normal file
124
src/lib/components/icons/ChatMenu.svelte
Normal file
@ -0,0 +1,124 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Tags from '$lib/components/chat/Tags.svelte';
|
||||
import Share from '$lib/components/icons/Share.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import Star from '$lib/components/icons/Star.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
export let pinHandler: Function;
|
||||
export let shareHandler: Function;
|
||||
export let cloneChatHandler: Function;
|
||||
export let archiveChatHandler: Function;
|
||||
export let renameHandler: Function;
|
||||
export let deleteHandler: Function;
|
||||
export let onClose: Function;
|
||||
|
||||
export let chatId = '';
|
||||
|
||||
let show = false;
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
bind:show
|
||||
on:change={(e) => {
|
||||
if (e.detail === false) {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Tooltip content={$i18n.t('More')}>
|
||||
<slot />
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu.Content
|
||||
class="w-full max-w-[180px] 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={-2}
|
||||
side="bottom"
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<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={() => {
|
||||
pinHandler();
|
||||
}}
|
||||
>
|
||||
<Star strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Pin')}</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={() => {
|
||||
renameHandler();
|
||||
}}
|
||||
>
|
||||
<Pencil strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Rename')}</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={() => {
|
||||
cloneChatHandler();
|
||||
}}
|
||||
>
|
||||
<DocumentDuplicate strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Clone')}</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={() => {
|
||||
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={() => {
|
||||
shareHandler();
|
||||
}}
|
||||
>
|
||||
<Share />
|
||||
<div class="flex items-center">{$i18n.t('Share')}</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={() => {
|
||||
deleteHandler();
|
||||
}}
|
||||
>
|
||||
<GarbageBin strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Delete')}</div>
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<hr class="border-gray-100 dark:border-gray-800 mt-2.5 mb-1.5" />
|
||||
|
||||
<div class="flex p-1">
|
||||
<Tags
|
||||
{chatId}
|
||||
on:close={() => {
|
||||
show = false;
|
||||
onClose();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</div>
|
||||
</Dropdown>
|
19
src/lib/components/icons/Star.svelte
Normal file
19
src/lib/components/icons/Star.svelte
Normal file
@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let className = 'w-4 h-4';
|
||||
export let strokeWidth = '1.5';
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width={strokeWidth}
|
||||
stroke="currentColor"
|
||||
class={className}
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M11.48 3.499a.562.562 0 0 1 1.04 0l2.125 5.111a.563.563 0 0 0 .475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 0 0-.182.557l1.285 5.385a.562.562 0 0 1-.84.61l-4.725-2.885a.562.562 0 0 0-.586 0L6.982 20.54a.562.562 0 0 1-.84-.61l1.285-5.386a.562.562 0 0 0-.182-.557l-4.204-3.602a.562.562 0 0 1 .321-.988l5.518-.442a.563.563 0 0 0 .475-.345L11.48 3.5Z"
|
||||
/>
|
||||
</svg>
|
@ -10,7 +10,8 @@
|
||||
tags,
|
||||
showSidebar,
|
||||
mobile,
|
||||
showArchivedChats
|
||||
showArchivedChats,
|
||||
pinnedChats
|
||||
} from '$lib/stores';
|
||||
import { onMount, getContext, tick } from 'svelte';
|
||||
|
||||
@ -46,6 +47,7 @@
|
||||
|
||||
let showDeleteConfirm = false;
|
||||
let showDropdown = false;
|
||||
|
||||
let filteredChatList = [];
|
||||
|
||||
$: filteredChatList = $chats.filter((chat) => {
|
||||
@ -80,6 +82,8 @@
|
||||
});
|
||||
|
||||
showSidebar.set(window.innerWidth > BREAKPOINT);
|
||||
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
|
||||
let touchstart;
|
||||
@ -412,7 +416,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if $tags.length > 0}
|
||||
{#if $tags.filter((t) => t.name !== 'pinned').length > 0}
|
||||
<div class="px-2.5 mb-2 flex gap-1 flex-wrap">
|
||||
<button
|
||||
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||
@ -422,7 +426,7 @@
|
||||
>
|
||||
{$i18n.t('all')}
|
||||
</button>
|
||||
{#each $tags as tag}
|
||||
{#each $tags.filter((t) => t.name !== 'pinned') as tag}
|
||||
<button
|
||||
class="px-2.5 text-xs font-medium bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-800 transition rounded-full"
|
||||
on:click={async () => {
|
||||
@ -440,6 +444,38 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $pinnedChats.length > 0}
|
||||
<div class="pl-2 py-2 flex flex-col space-y-1">
|
||||
<div class="">
|
||||
<div class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium pb-1.5">
|
||||
{$i18n.t('Pinned')}
|
||||
</div>
|
||||
|
||||
{#each $pinnedChats as chat, idx}
|
||||
<ChatItem
|
||||
{chat}
|
||||
{shiftKey}
|
||||
selected={selectedChatId === chat.id}
|
||||
on:select={() => {
|
||||
selectedChatId = chat.id;
|
||||
}}
|
||||
on:unselect={() => {
|
||||
selectedChatId = null;
|
||||
}}
|
||||
on:delete={(e) => {
|
||||
if ((e?.detail ?? '') === 'shift') {
|
||||
deleteChatHandler(chat.id);
|
||||
} else {
|
||||
deleteChat = chat;
|
||||
showDeleteConfirm = true;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="pl-2 my-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
|
||||
{#each filteredChatList as chat, idx}
|
||||
{#if idx === 0 || (idx > 0 && chat.time_range !== filteredChatList[idx - 1].time_range)}
|
||||
|
@ -11,9 +11,10 @@
|
||||
cloneChatById,
|
||||
deleteChatById,
|
||||
getChatList,
|
||||
getChatListByTagName,
|
||||
updateChatById
|
||||
} from '$lib/apis/chats';
|
||||
import { chatId, chats, mobile, showSidebar } from '$lib/stores';
|
||||
import { chatId, chats, mobile, pinnedChats, showSidebar } from '$lib/stores';
|
||||
|
||||
import ChatMenu from './ChatMenu.svelte';
|
||||
import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
|
||||
@ -40,6 +41,7 @@
|
||||
title: _title
|
||||
});
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
}
|
||||
};
|
||||
|
||||
@ -52,12 +54,14 @@
|
||||
if (res) {
|
||||
goto(`/c/${res.id}`);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
}
|
||||
};
|
||||
|
||||
const archiveChatHandler = async (id) => {
|
||||
await archiveChatById(localStorage.token, id);
|
||||
await chats.set(await getChatList(localStorage.token));
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
};
|
||||
|
||||
const focusEdit = async (node: HTMLInputElement) => {
|
||||
@ -233,6 +237,9 @@
|
||||
onClose={() => {
|
||||
dispatch('unselect');
|
||||
}}
|
||||
on:change={async () => {
|
||||
await pinnedChats.set(await getChatListByTagName(localStorage.token, 'pinned'));
|
||||
}}
|
||||
>
|
||||
<button
|
||||
aria-label="Chat Menu"
|
||||
|
@ -1,7 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu } from 'bits-ui';
|
||||
import { flyAndScale } from '$lib/utils/transitions';
|
||||
import { getContext } from 'svelte';
|
||||
import { getContext, createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
|
||||
@ -11,6 +13,9 @@
|
||||
import Share from '$lib/components/icons/Share.svelte';
|
||||
import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
|
||||
import DocumentDuplicate from '$lib/components/icons/DocumentDuplicate.svelte';
|
||||
import Bookmark from '$lib/components/icons/Bookmark.svelte';
|
||||
import BookmarkSlash from '$lib/components/icons/BookmarkSlash.svelte';
|
||||
import { addTagById, deleteTagById, getTagsById } from '$lib/apis/chats';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
@ -24,6 +29,28 @@
|
||||
export let chatId = '';
|
||||
|
||||
let show = false;
|
||||
let pinned = false;
|
||||
|
||||
const pinHandler = async () => {
|
||||
if (pinned) {
|
||||
await deleteTagById(localStorage.token, chatId, 'pinned');
|
||||
} else {
|
||||
await addTagById(localStorage.token, chatId, 'pinned');
|
||||
}
|
||||
dispatch('change');
|
||||
};
|
||||
|
||||
const checkPinned = async () => {
|
||||
pinned = (
|
||||
await getTagsById(localStorage.token, chatId).catch(async (error) => {
|
||||
return [];
|
||||
})
|
||||
).find((tag) => tag.name === 'pinned');
|
||||
};
|
||||
|
||||
$: if (show) {
|
||||
checkPinned();
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dropdown
|
||||
@ -46,6 +73,21 @@
|
||||
align="start"
|
||||
transition={flyAndScale}
|
||||
>
|
||||
<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={() => {
|
||||
pinHandler();
|
||||
}}
|
||||
>
|
||||
{#if pinned}
|
||||
<BookmarkSlash strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Unpin')}</div>
|
||||
{:else}
|
||||
<Bookmark strokeWidth="2" />
|
||||
<div class="flex items-center">{$i18n.t('Pin')}</div>
|
||||
{/if}
|
||||
</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={() => {
|
||||
|
@ -22,6 +22,7 @@ export const theme = writable('system');
|
||||
export const chatId = writable('');
|
||||
|
||||
export const chats = writable([]);
|
||||
export const pinnedChats = writable([]);
|
||||
export const tags = writable([]);
|
||||
|
||||
export const models: Writable<Model[]> = writable([]);
|
||||
|
Loading…
Reference in New Issue
Block a user