feat: shortcut delete and clean thread (#3423)

This commit is contained in:
Faisal Amir 2024-08-21 14:16:12 +07:00 committed by GitHub
parent 9aa3a61347
commit dc8acc0d49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 201 additions and 109 deletions

View File

@ -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,
])

View File

@ -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<boolean>(false)
/**
@ -138,3 +144,11 @@ export const activeSettingInputBoxAtom = atomWithStorage<boolean>(
ACTIVE_SETTING_INPUT_BOX,
false
)
export const modalActionThreadAtom = atom<{
showModal: ThreadModalAction | undefined
thread: Thread | undefined
}>({
showModal: undefined,
thread: undefined,
})

View File

@ -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'],

View File

@ -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<HTMLButtonElement, 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 (
<Modal
title="Clean Thread"
onOpenChange={(open) => {
if (open && closeContextMenu) {
closeContextMenu()
}
}}
trigger={
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
onClick={(e) => e.stopPropagation()}
>
<Paintbrush
size={16}
className="text-[hsla(var(--text-secondary))]"
/>
<span className="text-bold text-[hsla(var(--app-text-primary))]">
Clean thread
</span>
</div>
}
open={modalActionThread.showModal === ThreadModalAction.Clean}
onOpenChange={onCloseModal}
content={
<div>
<p className="text-[hsla(var(--text-secondary))]">
Are you sure you want to clean this thread?
</p>
<div className="mt-4 flex justify-end gap-x-2">
<ModalClose asChild onClick={(e) => e.stopPropagation()}>
<ModalClose
asChild
onClick={(e) => {
onCloseModal()
e.stopPropagation()
}}
>
<Button theme="ghost">No</Button>
</ModalClose>
<ModalClose asChild>

View File

@ -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<HTMLButtonElement, 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 (
<Modal
title="Delete Thread"
onOpenChange={(open) => {
if (open && closeContextMenu) {
closeContextMenu()
}
}}
trigger={
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
onClick={(e) => e.stopPropagation()}
>
<Trash2Icon
size={16}
className="text-[hsla(var(--destructive-bg))]"
/>
<span className="text-bold text-[hsla(var(--destructive-bg))]">
Delete thread
</span>
</div>
}
onOpenChange={onCloseModal}
open={modalActionThread.showModal === ThreadModalAction.Delete}
content={
<div>
<p className="text-[hsla(var(--text-secondary))]">

View File

@ -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<HTMLButtonElement, 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 (
<Modal
title="Edit title thread"
onOpenChange={(open) => {
if (open && closeContextMenu) {
closeContextMenu()
}
}}
trigger={
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
onClick={(e) => e.stopPropagation()}
>
<PencilIcon size={16} className="text-[hsla(var(--secondary))]" />
<span className="text-bold text-[hsla(var(--secondary))]">
Edit title
</span>
</div>
}
onOpenChange={onCloseModal}
open={modalActionThread.showModal === ThreadModalAction.EditTitle}
content={
<form className="mt-4">
<Input
@ -64,11 +59,7 @@ const ModalEditTitleThread = ({ thread, closeContextMenu }: Props) => {
<Button theme="ghost">Cancel</Button>
</ModalClose>
<ModalClose asChild>
<Button
type="submit"
onClick={onUpdateTitle}
disabled={title.length === 0}
>
<Button type="submit" onClick={onUpdateTitle} disabled={!title}>
Save
</Button>
</ModalClose>

View File

@ -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'
)}
>
<ModalEditTitleThread
thread={thread}
closeContextMenu={closeContextMenu}
/>
<ModalCleanThread
threadId={thread.id}
closeContextMenu={closeContextMenu}
/>
<ModalDeleteThread
threadId={thread.id}
closeContextMenu={closeContextMenu}
/>
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
onClick={(e) => {
setModalActionThread({
showModal: ThreadModalAction.EditTitle,
thread,
})
e.stopPropagation()
}}
>
<PencilIcon
size={16}
className="text-[hsla(var(--secondary))]"
/>
<span className="text-bold text-[hsla(var(--secondary))]">
Edit title
</span>
</div>
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
onClick={(e) => {
setModalActionThread({
showModal: ThreadModalAction.Clean,
thread,
})
e.stopPropagation()
}}
>
<Paintbrush
size={16}
className="text-[hsla(var(--text-secondary))]"
/>
<span className="text-bold text-[hsla(var(--app-text-primary))]">
Clean thread
</span>
</div>
<div
className="flex cursor-pointer items-center space-x-2 px-4 py-2 hover:bg-[hsla(var(--dropdown-menu-hover-bg))]"
onClick={(e) => {
setModalActionThread({
showModal: ThreadModalAction.Delete,
thread,
})
e.stopPropagation()
}}
>
<Trash2Icon
size={16}
className="text-[hsla(var(--destructive-bg))]"
/>
<span className="text-bold text-[hsla(var(--destructive-bg))]">
Delete thread
</span>
</div>
</div>
</div>
{activeThreadId === thread.id && (

View File

@ -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 = () => {
<ThreadLeftPanel />
<ThreadCenterPanel />
<ThreadRightPanel />
{/* Showing variant modal action for thread screen */}
<ModalEditTitleThread />
<ModalCleanThread />
<ModalDeleteThread />
</div>
)
}