From dc8acc0d491919139be9d5ca8a401bdc7e7d3bb8 Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Wed, 21 Aug 2024 14:16:12 +0700 Subject: [PATCH] feat: shortcut delete and clean thread (#3423) --- web/containers/Providers/KeyListener.tsx | 33 +++++++- web/helpers/atoms/Thread.atom.ts | 14 ++++ web/screens/Settings/Hotkeys/index.tsx | 10 +++ .../ModalCleanThread/index.tsx | 56 ++++++------- .../ModalDeleteThread/index.tsx | 47 +++++------ .../ModalEditTitleThread/index.tsx | 61 ++++++-------- web/screens/Thread/ThreadLeftPanel/index.tsx | 81 +++++++++++++++---- web/screens/Thread/index.tsx | 8 ++ 8 files changed, 201 insertions(+), 109 deletions(-) diff --git a/web/containers/Providers/KeyListener.tsx b/web/containers/Providers/KeyListener.tsx index 2731846df..39b516f1b 100644 --- a/web/containers/Providers/KeyListener.tsx +++ b/web/containers/Providers/KeyListener.tsx @@ -2,7 +2,7 @@ import { Fragment, ReactNode, useEffect } from 'react' -import { useAtomValue, useSetAtom } from 'jotai' +import { useAtom, useAtomValue, useSetAtom } from 'jotai' import { MainViewState } from '@/constants/screens' @@ -14,6 +14,11 @@ import { showRightPanelAtom, } from '@/helpers/atoms/App.atom' import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' +import { + activeThreadAtom, + modalActionThreadAtom, + ThreadModalAction, +} from '@/helpers/atoms/Thread.atom' type Props = { children: ReactNode @@ -22,9 +27,11 @@ type Props = { export default function KeyListener({ children }: Props) { const setShowLeftPanel = useSetAtom(showLeftPanelAtom) const setShowRightPanel = useSetAtom(showRightPanelAtom) - const setMainViewState = useSetAtom(mainViewStateAtom) + const [mainViewState, setMainViewState] = useAtom(mainViewStateAtom) const { requestCreateNewThread } = useCreateNewThread() const assistants = useAtomValue(assistantsAtom) + const activeThread = useAtomValue(activeThreadAtom) + const setModalActionThread = useSetAtom(modalActionThreadAtom) useEffect(() => { const onKeyDown = (e: KeyboardEvent) => { @@ -35,7 +42,26 @@ export default function KeyListener({ children }: Props) { return } + if (e.key === 'Backspace' && prefixKey && e.shiftKey) { + if (!activeThread || mainViewState !== MainViewState.Thread) return + setModalActionThread({ + showModal: ThreadModalAction.Delete, + thread: activeThread, + }) + return + } + + if (e.key === 'c' && prefixKey && e.shiftKey) { + if (!activeThread || mainViewState !== MainViewState.Thread) return + setModalActionThread({ + showModal: ThreadModalAction.Clean, + thread: activeThread, + }) + return + } + if (e.key === 'n' && prefixKey) { + if (mainViewState !== MainViewState.Thread) return requestCreateNewThread(assistants[0]) setMainViewState(MainViewState.Thread) return @@ -54,9 +80,12 @@ export default function KeyListener({ children }: Props) { document.addEventListener('keydown', onKeyDown) return () => document.removeEventListener('keydown', onKeyDown) }, [ + activeThread, assistants, + mainViewState, requestCreateNewThread, setMainViewState, + setModalActionThread, setShowLeftPanel, setShowRightPanel, ]) diff --git a/web/helpers/atoms/Thread.atom.ts b/web/helpers/atoms/Thread.atom.ts index ef7a88e17..57d9d08cb 100644 --- a/web/helpers/atoms/Thread.atom.ts +++ b/web/helpers/atoms/Thread.atom.ts @@ -9,6 +9,12 @@ import { import { atom } from 'jotai' import { atomWithStorage } from 'jotai/utils' +export enum ThreadModalAction { + Clean = 'clean', + Delete = 'delete', + EditTitle = 'edit-title', +} + export const engineParamsUpdateAtom = atom(false) /** @@ -138,3 +144,11 @@ export const activeSettingInputBoxAtom = atomWithStorage( ACTIVE_SETTING_INPUT_BOX, false ) + +export const modalActionThreadAtom = atom<{ + showModal: ThreadModalAction | undefined + thread: Thread | undefined +}>({ + showModal: undefined, + thread: undefined, +}) diff --git a/web/screens/Settings/Hotkeys/index.tsx b/web/screens/Settings/Hotkeys/index.tsx index 3a416e7ce..79227651e 100644 --- a/web/screens/Settings/Hotkeys/index.tsx +++ b/web/screens/Settings/Hotkeys/index.tsx @@ -16,6 +16,16 @@ const availableHotkeys = [ modifierKeys: [isMac ? '⌘' : 'Ctrl'], description: 'Toggle right panel', }, + { + combination: 'Shift Backspace', + modifierKeys: [isMac ? '⌘' : 'Ctrl'], + description: 'Delete current active thread', + }, + { + combination: 'Shift C', + modifierKeys: [isMac ? '⌘' : 'Ctrl'], + description: 'Clean current active thread', + }, { combination: ',', modifierKeys: [isMac ? '⌘' : 'Ctrl'], diff --git a/web/screens/Thread/ThreadLeftPanel/ModalCleanThread/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalCleanThread/index.tsx index 03878254f..81b1ce157 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalCleanThread/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalCleanThread/index.tsx @@ -1,54 +1,54 @@ import { useCallback, memo } from 'react' import { Button, Modal, ModalClose } from '@janhq/joi' -import { Paintbrush } from 'lucide-react' +import { useAtom } from 'jotai' import useDeleteThread from '@/hooks/useDeleteThread' -type Props = { - threadId: string - closeContextMenu?: () => void -} +import { + modalActionThreadAtom, + ThreadModalAction, +} from '@/helpers/atoms/Thread.atom' -const ModalCleanThread = ({ threadId, closeContextMenu }: Props) => { +const ModalCleanThread = () => { const { cleanThread } = useDeleteThread() + const [modalActionThread, setModalActionThread] = useAtom( + modalActionThreadAtom + ) + const onCleanThreadClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation() - cleanThread(threadId) + cleanThread(modalActionThread.thread?.id as string) }, - [cleanThread, threadId] + [cleanThread, modalActionThread.thread?.id] ) + const onCloseModal = useCallback(() => { + setModalActionThread({ + showModal: undefined, + thread: undefined, + }) + }, [setModalActionThread]) + return ( { - if (open && closeContextMenu) { - closeContextMenu() - } - }} - trigger={ -
e.stopPropagation()} - > - - - Clean thread - -
- } + open={modalActionThread.showModal === ThreadModalAction.Clean} + onOpenChange={onCloseModal} content={

Are you sure you want to clean this thread?

- e.stopPropagation()}> + { + onCloseModal() + e.stopPropagation() + }} + > diff --git a/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx index 75c158c87..84656ad87 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalDeleteThread/index.tsx @@ -1,48 +1,41 @@ import { useCallback, memo } from 'react' import { Modal, ModalClose, Button } from '@janhq/joi' -import { Trash2Icon } from 'lucide-react' +import { useAtom } from 'jotai' import useDeleteThread from '@/hooks/useDeleteThread' -type Props = { - threadId: string - closeContextMenu?: () => void -} +import { + modalActionThreadAtom, + ThreadModalAction, +} from '@/helpers/atoms/Thread.atom' -const ModalDeleteThread = ({ threadId, closeContextMenu }: Props) => { +const ModalDeleteThread = () => { const { deleteThread } = useDeleteThread() + const [modalActionThread, setModalActionThread] = useAtom( + modalActionThreadAtom + ) const onDeleteThreadClick = useCallback( (e: React.MouseEvent) => { e.stopPropagation() - deleteThread(threadId) + deleteThread(modalActionThread.thread?.id as string) }, - [deleteThread, threadId] + [deleteThread, modalActionThread.thread?.id] ) + const onCloseModal = useCallback(() => { + setModalActionThread({ + showModal: undefined, + thread: undefined, + }) + }, [setModalActionThread]) + return ( { - if (open && closeContextMenu) { - closeContextMenu() - } - }} - trigger={ -
e.stopPropagation()} - > - - - Delete thread - -
- } + onOpenChange={onCloseModal} + open={modalActionThread.showModal === ThreadModalAction.Delete} content={

diff --git a/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx b/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx index 297c8f182..ddeaedf40 100644 --- a/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/ModalEditTitleThread/index.tsx @@ -1,57 +1,52 @@ import { useCallback, useLayoutEffect, memo, useState } from 'react' -import { Thread } from '@janhq/core' import { Modal, ModalClose, Button, Input } from '@janhq/joi' -import { PencilIcon } from 'lucide-react' +import { useAtom } from 'jotai' import { useCreateNewThread } from '@/hooks/useCreateNewThread' -type Props = { - thread: Thread - closeContextMenu?: () => void -} +import { + modalActionThreadAtom, + ThreadModalAction, +} from '@/helpers/atoms/Thread.atom' -const ModalEditTitleThread = ({ thread, closeContextMenu }: Props) => { - const [title, setTitle] = useState(thread.title) +const ModalEditTitleThread = () => { const { updateThreadMetadata } = useCreateNewThread() + const [modalActionThread, setModalActionThread] = useAtom( + modalActionThreadAtom + ) + const [title, setTitle] = useState(modalActionThread.thread?.title as string) useLayoutEffect(() => { - if (thread.title) { - setTitle(thread.title) + if (modalActionThread.thread?.title) { + setTitle(modalActionThread.thread?.title) } - }, [thread.title]) + }, [modalActionThread.thread?.title]) const onUpdateTitle = useCallback( (e: React.MouseEvent) => { e.stopPropagation() - + if (!modalActionThread.thread) return null updateThreadMetadata({ - ...thread, + ...modalActionThread?.thread, title: title || 'New Thread', }) }, - [thread, title, updateThreadMetadata] + [modalActionThread?.thread, title, updateThreadMetadata] ) + const onCloseModal = useCallback(() => { + setModalActionThread({ + showModal: undefined, + thread: undefined, + }) + }, [setModalActionThread]) + return ( { - if (open && closeContextMenu) { - closeContextMenu() - } - }} - trigger={ -

e.stopPropagation()} - > - - - Edit title - -
- } + onOpenChange={onCloseModal} + open={modalActionThread.showModal === ThreadModalAction.EditTitle} content={
{ - diff --git a/web/screens/Thread/ThreadLeftPanel/index.tsx b/web/screens/Thread/ThreadLeftPanel/index.tsx index 2f7997509..0b866ea26 100644 --- a/web/screens/Thread/ThreadLeftPanel/index.tsx +++ b/web/screens/Thread/ThreadLeftPanel/index.tsx @@ -5,7 +5,13 @@ import { Thread } from '@janhq/core' import { Button } from '@janhq/joi' import { motion as m } from 'framer-motion' import { useAtomValue, useSetAtom } from 'jotai' -import { GalleryHorizontalEndIcon, MoreHorizontalIcon } from 'lucide-react' +import { + GalleryHorizontalEndIcon, + MoreHorizontalIcon, + Paintbrush, + PencilIcon, + Trash2Icon, +} from 'lucide-react' import { twMerge } from 'tailwind-merge' @@ -15,16 +21,14 @@ import { useCreateNewThread } from '@/hooks/useCreateNewThread' import useRecommendedModel from '@/hooks/useRecommendedModel' import useSetActiveThread from '@/hooks/useSetActiveThread' -import ModalCleanThread from './ModalCleanThread' -import ModalDeleteThread from './ModalDeleteThread' -import ModalEditTitleThread from './ModalEditTitleThread' - import { assistantsAtom } from '@/helpers/atoms/Assistant.atom' import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom' import { getActiveThreadIdAtom, + modalActionThreadAtom, threadDataReadyAtom, + ThreadModalAction, threadsAtom, } from '@/helpers/atoms/Thread.atom' @@ -37,6 +41,7 @@ const ThreadLeftPanel = () => { const { requestCreateNewThread } = useCreateNewThread() const setEditMessage = useSetAtom(editMessageAtom) const { recommendedModel, downloadedModels } = useRecommendedModel() + const setModalActionThread = useSetAtom(modalActionThreadAtom) const [contextMenu, setContextMenu] = useState<{ visible: boolean @@ -147,18 +152,60 @@ const ThreadLeftPanel = () => { 'visible' )} > - - - +
{ + setModalActionThread({ + showModal: ThreadModalAction.EditTitle, + thread, + }) + e.stopPropagation() + }} + > + + + Edit title + +
+
{ + setModalActionThread({ + showModal: ThreadModalAction.Clean, + thread, + }) + e.stopPropagation() + }} + > + + + Clean thread + +
+
{ + setModalActionThread({ + showModal: ThreadModalAction.Delete, + thread, + }) + e.stopPropagation() + }} + > + + + Delete thread + +
{activeThreadId === thread.id && ( diff --git a/web/screens/Thread/index.tsx b/web/screens/Thread/index.tsx index ca8aab333..ef125e924 100644 --- a/web/screens/Thread/index.tsx +++ b/web/screens/Thread/index.tsx @@ -1,6 +1,9 @@ import ThreadLeftPanel from '@/screens/Thread/ThreadLeftPanel' import ThreadCenterPanel from './ThreadCenterPanel' +import ModalCleanThread from './ThreadLeftPanel/ModalCleanThread' +import ModalDeleteThread from './ThreadLeftPanel/ModalDeleteThread' +import ModalEditTitleThread from './ThreadLeftPanel/ModalEditTitleThread' import ThreadRightPanel from './ThreadRightPanel' const ThreadScreen = () => { @@ -9,6 +12,11 @@ const ThreadScreen = () => { + + {/* Showing variant modal action for thread screen */} + + +
) }