123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443 |
- <script lang="ts">
- import { toast } from 'svelte-sonner';
- import { goto, invalidate, invalidateAll } from '$app/navigation';
- import { onMount, getContext, createEventDispatcher, tick, onDestroy } from 'svelte';
- const i18n = getContext('i18n');
- const dispatch = createEventDispatcher();
- import {
- archiveChatById,
- cloneChatById,
- deleteChatById,
- getAllTags,
- getChatById,
- getChatList,
- getChatListByTagName,
- getPinnedChatList,
- updateChatById
- } from '$lib/apis/chats';
- import {
- chatId,
- chatTitle as _chatTitle,
- chats,
- mobile,
- pinnedChats,
- showSidebar,
- currentChatPage,
- tags
- } from '$lib/stores';
- import ChatMenu from './ChatMenu.svelte';
- import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
- import ShareChatModal from '$lib/components/chat/ShareChatModal.svelte';
- import GarbageBin from '$lib/components/icons/GarbageBin.svelte';
- import Tooltip from '$lib/components/common/Tooltip.svelte';
- import ArchiveBox from '$lib/components/icons/ArchiveBox.svelte';
- import DragGhost from '$lib/components/common/DragGhost.svelte';
- import Check from '$lib/components/icons/Check.svelte';
- import XMark from '$lib/components/icons/XMark.svelte';
- import Document from '$lib/components/icons/Document.svelte';
- export let className = '';
- export let id;
- export let title;
- export let selected = false;
- export let shiftKey = false;
- let chat = null;
- let mouseOver = false;
- let draggable = false;
- $: if (mouseOver) {
- loadChat();
- }
- const loadChat = async () => {
- if (!chat) {
- draggable = false;
- chat = await getChatById(localStorage.token, id);
- draggable = true;
- }
- };
- let showShareChatModal = false;
- let confirmEdit = false;
- let chatTitle = title;
- const editChatTitle = async (id, title) => {
- if (title === '') {
- toast.error($i18n.t('Title cannot be an empty string.'));
- } else {
- await updateChatById(localStorage.token, id, {
- title: title
- });
- if (id === $chatId) {
- _chatTitle.set(title);
- }
- currentChatPage.set(1);
- await chats.set(await getChatList(localStorage.token, $currentChatPage));
- await pinnedChats.set(await getPinnedChatList(localStorage.token));
- }
- };
- const cloneChatHandler = async (id) => {
- const res = await cloneChatById(
- localStorage.token,
- id,
- $i18n.t('Clone of {{TITLE}}', {
- TITLE: title
- })
- ).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
- if (res) {
- goto(`/c/${res.id}`);
- currentChatPage.set(1);
- await chats.set(await getChatList(localStorage.token, $currentChatPage));
- await pinnedChats.set(await getPinnedChatList(localStorage.token));
- }
- };
- const deleteChatHandler = async (id) => {
- const res = await deleteChatById(localStorage.token, id).catch((error) => {
- toast.error(`${error}`);
- return null;
- });
- if (res) {
- tags.set(await getAllTags(localStorage.token));
- if ($chatId === id) {
- await goto('/');
- await chatId.set('');
- await tick();
- }
- dispatch('change');
- }
- };
- const archiveChatHandler = async (id) => {
- await archiveChatById(localStorage.token, id);
- dispatch('change');
- };
- const focusEdit = async (node: HTMLInputElement) => {
- node.focus();
- };
- let itemElement;
- let dragged = false;
- let x = 0;
- let y = 0;
- const dragImage = new Image();
- dragImage.src =
- 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
- const onDragStart = (event) => {
- event.stopPropagation();
- event.dataTransfer.setDragImage(dragImage, 0, 0);
- // Set the data to be transferred
- event.dataTransfer.setData(
- 'text/plain',
- JSON.stringify({
- type: 'chat',
- id: id,
- item: chat
- })
- );
- dragged = true;
- itemElement.style.opacity = '0.5'; // Optional: Visual cue to show it's being dragged
- };
- const onDrag = (event) => {
- event.stopPropagation();
- x = event.clientX;
- y = event.clientY;
- };
- const onDragEnd = (event) => {
- event.stopPropagation();
- itemElement.style.opacity = '1'; // Reset visual cue after drag
- dragged = false;
- };
- onMount(() => {
- if (itemElement) {
- // Event listener for when dragging starts
- itemElement.addEventListener('dragstart', onDragStart);
- // Event listener for when dragging occurs (optional)
- itemElement.addEventListener('drag', onDrag);
- // Event listener for when dragging ends
- itemElement.addEventListener('dragend', onDragEnd);
- }
- });
- onDestroy(() => {
- if (itemElement) {
- itemElement.removeEventListener('dragstart', onDragStart);
- itemElement.removeEventListener('drag', onDrag);
- itemElement.removeEventListener('dragend', onDragEnd);
- }
- });
- let showDeleteConfirm = false;
- </script>
- <ShareChatModal bind:show={showShareChatModal} chatId={id} />
- <DeleteConfirmDialog
- bind:show={showDeleteConfirm}
- title={$i18n.t('Delete chat?')}
- on:confirm={() => {
- deleteChatHandler(id);
- }}
- >
- <div class=" text-sm text-gray-500 flex-1 line-clamp-3">
- {$i18n.t('This will delete')} <span class=" font-semibold">{title}</span>.
- </div>
- </DeleteConfirmDialog>
- {#if dragged && x && y}
- <DragGhost {x} {y}>
- <div class=" bg-black/80 backdrop-blur-2xl px-2 py-1 rounded-lg w-fit max-w-40">
- <div class="flex items-center gap-1">
- <Document className=" size-[18px]" strokeWidth="2" />
- <div class=" text-xs text-white line-clamp-1">
- {title}
- </div>
- </div>
- </div>
- </DragGhost>
- {/if}
- <div
- bind:this={itemElement}
- class=" w-full {className} relative group"
- draggable={draggable && !confirmEdit}
- >
- {#if confirmEdit}
- <div
- class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
- confirmEdit
- ? 'bg-gray-200 dark:bg-gray-900'
- : selected
- ? 'bg-gray-100 dark:bg-gray-950'
- : 'group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
- >
- <input
- use:focusEdit
- bind:value={chatTitle}
- id="chat-title-input-{id}"
- class=" bg-transparent w-full outline-hidden mr-10"
- />
- </div>
- {:else}
- <a
- class=" w-full flex justify-between rounded-lg px-[11px] py-[6px] {id === $chatId ||
- confirmEdit
- ? 'bg-gray-200 dark:bg-gray-900'
- : selected
- ? 'bg-gray-100 dark:bg-gray-950'
- : ' group-hover:bg-gray-100 dark:group-hover:bg-gray-950'} whitespace-nowrap text-ellipsis"
- href="/c/{id}"
- on:click={() => {
- dispatch('select');
- if ($mobile) {
- showSidebar.set(false);
- }
- }}
- on:dblclick={() => {
- chatTitle = title;
- confirmEdit = true;
- }}
- on:mouseenter={(e) => {
- mouseOver = true;
- }}
- on:mouseleave={(e) => {
- mouseOver = false;
- }}
- on:focus={(e) => {}}
- draggable="false"
- >
- <div class=" flex self-center flex-1 w-full">
- <div dir="auto" class="text-left self-center overflow-hidden w-full h-[20px]">
- {title}
- </div>
- </div>
- </a>
- {/if}
- <!-- svelte-ignore a11y-no-static-element-interactions -->
- <div
- class="
- {id === $chatId || confirmEdit
- ? 'from-gray-200 dark:from-gray-900'
- : selected
- ? 'from-gray-100 dark:from-gray-950'
- : 'invisible group-hover:visible from-gray-100 dark:from-gray-950'}
- absolute {className === 'pr-2'
- ? 'right-[8px]'
- : 'right-0'} top-[4px] py-1 pr-0.5 mr-1.5 pl-5 bg-linear-to-l from-80%
- to-transparent"
- on:mouseenter={(e) => {
- mouseOver = true;
- }}
- on:mouseleave={(e) => {
- mouseOver = false;
- }}
- >
- {#if confirmEdit}
- <div
- class="flex self-center items-center space-x-1.5 z-10 translate-y-[0.5px] -translate-x-[0.5px]"
- >
- <Tooltip content={$i18n.t('Confirm')}>
- <button
- class=" self-center dark:hover:text-white transition"
- on:click={() => {
- editChatTitle(id, chatTitle);
- confirmEdit = false;
- chatTitle = '';
- }}
- >
- <Check className=" size-3.5" strokeWidth="2.5" />
- </button>
- </Tooltip>
- <Tooltip content={$i18n.t('Cancel')}>
- <button
- class=" self-center dark:hover:text-white transition"
- on:click={() => {
- confirmEdit = false;
- chatTitle = '';
- }}
- >
- <XMark strokeWidth="2.5" />
- </button>
- </Tooltip>
- </div>
- {:else if shiftKey && mouseOver}
- <div class=" flex items-center self-center space-x-1.5">
- <Tooltip content={$i18n.t('Archive')} className="flex items-center">
- <button
- class=" self-center dark:hover:text-white transition"
- on:click={() => {
- archiveChatHandler(id);
- }}
- type="button"
- >
- <ArchiveBox className="size-4 translate-y-[0.5px]" strokeWidth="2" />
- </button>
- </Tooltip>
- <Tooltip content={$i18n.t('Delete')}>
- <button
- class=" self-center dark:hover:text-white transition"
- on:click={() => {
- deleteChatHandler(id);
- }}
- type="button"
- >
- <GarbageBin strokeWidth="2" />
- </button>
- </Tooltip>
- </div>
- {:else}
- <div class="flex self-center space-x-1 z-10">
- <ChatMenu
- chatId={id}
- cloneChatHandler={() => {
- cloneChatHandler(id);
- }}
- shareHandler={() => {
- showShareChatModal = true;
- }}
- archiveChatHandler={() => {
- archiveChatHandler(id);
- }}
- renameHandler={async () => {
- chatTitle = title;
- confirmEdit = true;
- await tick();
- const input = document.getElementById(`chat-title-input-${id}`);
- if (input) {
- input.focus();
- }
- }}
- deleteHandler={() => {
- showDeleteConfirm = true;
- }}
- onClose={() => {
- dispatch('unselect');
- }}
- on:change={async () => {
- dispatch('change');
- }}
- on:tag={(e) => {
- dispatch('tag', e.detail);
- }}
- >
- <button
- aria-label="Chat Menu"
- class=" self-center dark:hover:text-white transition"
- on:click={() => {
- dispatch('select');
- }}
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- class="w-4 h-4"
- >
- <path
- d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
- />
- </svg>
- </button>
- </ChatMenu>
- {#if id === $chatId}
- <!-- Shortcut support using "delete-chat-button" id -->
- <button
- id="delete-chat-button"
- class="hidden"
- on:click={() => {
- showDeleteConfirm = true;
- }}
- >
- <svg
- xmlns="http://www.w3.org/2000/svg"
- viewBox="0 0 16 16"
- fill="currentColor"
- class="w-4 h-4"
- >
- <path
- d="M2 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM6.5 8a1.5 1.5 0 1 1 3 0 1.5 1.5 0 0 1-3 0ZM12.5 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z"
- />
- </svg>
- </button>
- {/if}
- </div>
- {/if}
- </div>
- </div>
|