feat: shortcut delete and clean thread (#3423)
This commit is contained in:
parent
9aa3a61347
commit
dc8acc0d49
@ -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,
|
||||
])
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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'],
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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))]">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user