enh: drag and drop chat to pin

This commit is contained in:
Timothy J. Baek 2024-10-15 02:12:39 -07:00
parent b01f9e8ec3
commit 2f4c04055c
4 changed files with 195 additions and 101 deletions

View File

@ -22,8 +22,9 @@
<div
bind:this={popupElement}
class=" absolute text-white z-[99999]"
style="top: {y}px; left: {x}px;"
class="fixed top-0 left-0 w-screen h-[100dvh] z-50 touch-none pointer-events-none"
>
<slot></slot>
<div class=" absolute text-white z-[99999]" style="top: {y}px; left: {x}px;">
<slot></slot>
</div>
</div>

View File

@ -1,5 +1,5 @@
<script>
import { getContext, createEventDispatcher } from 'svelte';
import { getContext, createEventDispatcher, onMount, onDestroy } from 'svelte';
const i18n = getContext('i18n');
const dispatch = createEventDispatcher();
@ -9,48 +9,94 @@
import Collapsible from './Collapsible.svelte';
export let open = true;
export let id = '';
export let name = '';
export let collapsible = true;
let folderElement;
let dragged = false;
const onDragOver = (e) => {
e.preventDefault();
dragged = true;
};
const onDrop = (e) => {
e.preventDefault();
if (folderElement.contains(e.target)) {
console.log('Dropped on the Button');
// get data from the drag event
const dataTransfer = e.dataTransfer.getData('text/plain');
const data = JSON.parse(dataTransfer);
console.log(data);
dispatch('drop', data);
dragged = false;
}
};
const onDragLeave = (e) => {
e.preventDefault();
dragged = false;
};
onMount(() => {
folderElement.addEventListener('dragover', onDragOver);
folderElement.addEventListener('drop', onDrop);
folderElement.addEventListener('dragleave', onDragLeave);
});
onDestroy(() => {
folderElement.addEventListener('dragover', onDragOver);
folderElement.removeEventListener('drop', onDrop);
folderElement.removeEventListener('dragleave', onDragLeave);
});
</script>
<div>
<Collapsible
bind:open
className="w-full"
buttonClassName="w-full"
on:change={(e) => {
dispatch('change', e.detail);
}}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div bind:this={folderElement} class="relative">
{#if dragged}
<div
class="mx-2 w-full"
on:dragenter={(e) => {
e.stopPropagation();
class="absolute top-0 left-0 w-full h-full rounded-sm bg-gray-200 bg-opacity-50 dark:bg-opacity-10 z-50 pointer-events-none touch-none"
></div>
{/if}
{#if collapsible}
<Collapsible
bind:open
className="w-full "
buttonClassName="w-full"
on:change={(e) => {
dispatch('change', e.detail);
}}
on:drop={(e) => {
console.log('Dropped on the Button');
}}
on:dragleave={(e) => {}}
>
<button
class="w-full py-1 px-1.5 rounded-md flex items-center gap-1 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
>
<div class="text-gray-300">
{#if open}
<ChevronDown className=" size-3" strokeWidth="2.5" />
{:else}
<ChevronRight className=" size-3" strokeWidth="2.5" />
{/if}
</div>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="mx-2 w-full">
<button
class="w-full py-1 px-1.5 rounded-md flex items-center gap-1 text-xs text-gray-500 dark:text-gray-500 font-medium hover:bg-gray-100 dark:hover:bg-gray-900 transition"
>
<div class="text-gray-300">
{#if open}
<ChevronDown className=" size-3" strokeWidth="2.5" />
{:else}
<ChevronRight className=" size-3" strokeWidth="2.5" />
{/if}
</div>
<div class="translate-y-[0.5px]">
{name}
</div>
</button>
</div>
<div class="translate-y-[0.5px]">
{name}
</div>
</button>
</div>
<div slot="content">
<slot></slot>
</div>
</Collapsible>
<div slot="content">
<slot></slot>
</div>
</Collapsible>
{:else}
<slot></slot>
{/if}
</div>

View File

@ -35,7 +35,9 @@
cloneChatById,
getChatListBySearchText,
createNewChat,
getPinnedChatList
getPinnedChatList,
toggleChatPinnedStatusById,
getChatPinnedStatusById
} from '$lib/apis/chats';
import { WEBUI_BASE_URL } from '$lib/constants';
@ -363,8 +365,8 @@
bind:this={navElement}
id="sidebar"
class="h-screen max-h-[100dvh] min-h-screen select-none {$showSidebar
? '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
? 'md:relative w-[260px] max-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 overflow-x-hidden
"
data-state={$showSidebar}
>
@ -381,7 +383,7 @@
</div>
{/if}
<div
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] z-50 {$showSidebar
class="py-2.5 my-auto flex flex-col justify-between h-screen max-h-[100dvh] w-[260px] overflow-x-hidden z-50 {$showSidebar
? ''
: 'invisible'}"
>
@ -517,13 +519,27 @@
{/if}
{#if !search && $pinnedChats.length > 0}
<div class=" pb-2 flex flex-col space-y-1">
<div class=" flex flex-col space-y-1">
<Folder
bind:open={showPinnedChat}
on:change={(e) => {
localStorage.setItem('showPinnedChat', e.detail);
console.log(e.detail);
}}
on:drop={async (e) => {
const { id } = e.detail;
const status = await getChatPinnedStatusById(localStorage.token, id);
if (!status) {
const res = await toggleChatPinnedStatusById(localStorage.token, id);
if (res) {
await pinnedChats.set(await getPinnedChatList(localStorage.token));
initChatList();
}
}
}}
name={$i18n.t('Pinned')}
>
<div class="pl-2 mt-1 flex flex-col overflow-y-auto scrollbar-hidden">
@ -557,17 +573,36 @@
</div>
{/if}
<div class="pl-2 flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
{#if $chats}
{#each $chats as chat, idx}
{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
<div
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx === 0
? ''
: 'pt-5'} pb-0.5"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
<div class="flex-1 flex flex-col space-y-1 overflow-y-auto scrollbar-hidden">
<Folder
collapsible={false}
on:drop={async (e) => {
const { id } = e.detail;
const status = await getChatPinnedStatusById(localStorage.token, id);
if (status) {
const res = await toggleChatPinnedStatusById(localStorage.token, id);
if (res) {
await pinnedChats.set(await getPinnedChatList(localStorage.token));
initChatList();
}
}
}}
>
<div class="pt-2 pl-2">
{#if $chats}
{#each $chats as chat, idx}
{#if idx === 0 || (idx > 0 && chat.time_range !== $chats[idx - 1].time_range)}
<div
class="w-full pl-2.5 text-xs text-gray-500 dark:text-gray-500 font-medium {idx ===
0
? ''
: 'pt-5'} pb-0.5"
>
{$i18n.t(chat.time_range)}
<!-- localisation keys for time_range to be recognized from the i18next parser (so they don't get automatically removed):
{$i18n.t('Today')}
{$i18n.t('Yesterday')}
{$i18n.t('Previous 7 days')}
@ -585,54 +620,58 @@
{$i18n.t('November')}
{$i18n.t('December')}
-->
</div>
{/if}
</div>
{/if}
<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;
}
}}
on:tag={(e) => {
const { type, name } = e.detail;
tagEventHandler(type, name, chat.id);
}}
/>
{/each}
<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;
}
}}
on:tag={(e) => {
const { type, name } = e.detail;
tagEventHandler(type, name, chat.id);
}}
/>
{/each}
{#if $scrollPaginationEnabled && !allChatsLoaded}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadMoreChats();
}
}}
>
{#if $scrollPaginationEnabled && !allChatsLoaded}
<Loader
on:visible={(e) => {
if (!chatListLoading) {
loadMoreChats();
}
}}
>
<div
class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2"
>
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{/if}
{:else}
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
</div>
</Loader>
{/if}
{:else}
<div class="w-full flex justify-center py-1 text-xs animate-pulse items-center gap-2">
<Spinner className=" size-4" />
<div class=" ">Loading...</div>
{/if}
</div>
{/if}
</Folder>
</div>
</div>

View File

@ -104,6 +104,14 @@
const onDragStart = (event) => {
event.dataTransfer.setDragImage(dragImage, 0, 0);
// Set the data to be transferred
event.dataTransfer.setData(
'text/plain',
JSON.stringify({
id: chat.id
})
);
drag = true;
itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
};
@ -114,8 +122,8 @@
};
const onDragEnd = (event) => {
drag = false;
itemElement.style.opacity = '1'; // Reset visual cue after drag
drag = false;
};
onMount(() => {
@ -142,9 +150,9 @@
{#if drag && x && y}
<DragGhost {x} {y}>
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg">
<div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-44">
<div>
<div class=" text-xs text-white">
<div class=" text-xs text-white line-clamp-1">
{chat.title}
</div>
</div>
@ -169,7 +177,7 @@
</div>
{:else}
<a
class=" w-full flex justify-between rounded-xl px-3 py-2 {chat.id === $chatId || confirmEdit
class=" w-full flex justify-between rounded-lg px-3 py-2 {chat.id === $chatId || confirmEdit
? 'bg-gray-200 dark:bg-gray-900'
: selected
? 'bg-gray-100 dark:bg-gray-950'