Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add delete mode feature for multi message deletion #1542

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 18 additions & 15 deletions src/renderer/MainPane.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,32 @@
import { Box } from '@mui/material'
import * as atoms from './stores/atoms'
import { useAtomValue } from 'jotai'
import Header from './components/Header'
import InputBox from './components/InputBox'
import MessageList from './components/MessageList'
import { drawerWidth } from './Sidebar'
import Header from './components/Header'
import * as atoms from './stores/atoms'
import { MessageSelectionProvider } from './contexts/MessageSelectionContext'

interface Props {}

export default function MainPane(props: Props) {
const currentSession = useAtomValue(atoms.currentSessionAtom)

return (
<Box
className="h-full w-full"
sx={{
flexGrow: 1,
marginLeft: `${drawerWidth}px`,
}}
>
<div className="flex flex-col h-full">
<Header />
<MessageList />
<InputBox currentSessionId={currentSession.id} currentSessionType={currentSession.type || 'chat'} />
</div>
</Box>
<MessageSelectionProvider>
<Box
className="w-full h-full"
sx={{
flexGrow: 1,
marginLeft: `${drawerWidth}px`,
}}
>
<div className="flex flex-col h-full">
<Header />
<MessageList />
<InputBox currentSessionId={currentSession.id} currentSessionType={currentSession.type || 'chat'} />
</div>
</Box>
</MessageSelectionProvider>
)
}
49 changes: 30 additions & 19 deletions src/renderer/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import { useEffect } from 'react'
import { Typography, useTheme } from '@mui/material'
import { Typography, useTheme, IconButton, Tooltip } from '@mui/material'
import DeleteIcon from '@mui/icons-material/Delete'
import CheckIcon from '@mui/icons-material/Check'
import SponsorChip from './SponsorChip'
import * as atoms from '../stores/atoms'
import { useAtomValue, useSetAtom } from 'jotai'
import * as sessionActions from '../stores/sessionActions'
import Toolbar from './Toolbar'
import { cn } from '@/lib/utils'
import { useMessageSelectionContext } from '../contexts/MessageSelectionContext'

interface Props { }

export default function Header(props: Props) {
export default function Header() {
const theme = useTheme()
const currentSession = useAtomValue(atoms.currentSessionAtom)
const setChatConfigDialogSession = useSetAtom(atoms.chatConfigDialogAtom)
const { isDeleteMode, toggleDeleteMode, selectedMessages, handleDeleteMessages } = useMessageSelectionContext()

useEffect(() => {
if (
currentSession.name === 'Untitled'
&& currentSession.messages.length >= 2
) {
if (currentSession.name === 'Untitled' && currentSession.messages.length >= 2) {
sessionActions.generateName(currentSession.id)
return
return
}
}, [currentSession.messages.length])

Expand All @@ -30,14 +29,14 @@ export default function Header(props: Props) {

return (
<div
className="pt-3 pb-2 px-4"
className="px-4 pt-3 pb-2"
style={{
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
borderBottomColor: theme.palette.divider,
}}
>
<div className={cn('w-full mx-auto flex flex-row')}>
<div className={cn('flex flex-row mx-auto w-full')}>
<Typography
variant="h6"
color="inherit"
Expand All @@ -49,19 +48,31 @@ export default function Header(props: Props) {
textOverflow: 'ellipsis',
}}
className="flex items-center cursor-pointer"
onClick={() => {
editCurrentSession()
}}
onClick={editCurrentSession}
>
{
<Typography variant="h6" noWrap className={cn('max-w-56', 'ml-3')}>
{currentSession.name}
</Typography>
}
<Typography variant="h6" noWrap className={cn('max-w-56', 'ml-3')}>
{currentSession.name}
</Typography>
</Typography>
<SponsorChip sessionId={currentSession.id} />
{isDeleteMode && selectedMessages.size > 0 ? (
<IconButton color="primary" onClick={handleDeleteMessages} sx={{ mr: 2 }}>
<CheckIcon />
</IconButton>
) : null}
<Tooltip title={isDeleteMode ? 'Cancel delete mode' : 'Delete mode'}>
<IconButton
color={isDeleteMode ? 'secondary' : 'primary'}
onClick={toggleDeleteMode}
sx={{ mr: 2 }}
>
<DeleteIcon />
</IconButton>
</Tooltip>
<Toolbar />
</div>
</div>
)
}

export { Header }
108 changes: 60 additions & 48 deletions src/renderer/components/Message.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { useEffect, useState, useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import Box from '@mui/material/Box'
import Avatar from '@mui/material/Avatar'
import {
Typography,
Grid,
useTheme,
} from '@mui/material'
import { Typography, Grid, useTheme, Checkbox } from '@mui/material'
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'
import CheckBoxIcon from '@mui/icons-material/CheckBox'
import PersonIcon from '@mui/icons-material/Person'
import SmartToyIcon from '@mui/icons-material/SmartToy'
import SettingsIcon from '@mui/icons-material/Settings'
Expand All @@ -25,7 +23,7 @@ import * as scrollActions from '../stores/scrollActions'
import Markdown from '@/components/Markdown'
import '../static/Block.css'
import MessageErrTips from './MessageErrTips'
import * as dateFns from "date-fns"
import * as dateFns from 'date-fns'
import { cn } from '@/lib/utils'
import { estimateTokensFromMessages } from '@/packages/token'
import { countWord } from '@/packages/word-count'
Expand All @@ -39,6 +37,10 @@ export interface Props {
collapseThreshold?: number
hiddenButtonGroup?: boolean
small?: boolean
isDeleteMode: boolean
onSelectMessage: (id: string, selected: boolean) => void
isSystem: boolean
isSelected: boolean
}

export default function Message(props: Props) {
Expand All @@ -54,11 +56,12 @@ export default function Message(props: Props) {
const currentSessionPicUrl = useAtomValue(currsentSessionPicUrlAtom)
const setOpenSettingWindow = useSetAtom(openSettingDialogAtom)

const { msg, className, collapseThreshold, hiddenButtonGroup, small } = props
const { msg, className, collapseThreshold, hiddenButtonGroup, small, isSelected } = props

const needCollapse = collapseThreshold
&& (JSON.stringify(msg.content)).length > collapseThreshold
&& (JSON.stringify(msg.content)).length - collapseThreshold > 50
const needCollapse =
collapseThreshold &&
JSON.stringify(msg.content).length > collapseThreshold &&
JSON.stringify(msg.content).length - collapseThreshold > 50
const [isCollapsed, setIsCollapsed] = useState(needCollapse)

const ref = useRef<HTMLDivElement>(null)
Expand Down Expand Up @@ -115,7 +118,7 @@ export default function Message(props: Props) {

const CollapseButton = (
<span
className='cursor-pointer inline-block font-bold text-blue-500 hover:text-white hover:bg-blue-500'
className="inline-block font-bold text-blue-500 cursor-pointer hover:text-white hover:bg-blue-500"
onClick={() => setIsCollapsed(!isCollapsed)}
>
[{isCollapsed ? t('Expand') : t('Collapse')}]
Expand All @@ -137,7 +140,7 @@ export default function Message(props: Props) {
system: 'system-msg',
assistant: 'assistant-msg',
}[msg?.role || 'user'],
className,
className
)}
sx={{
margin: '0',
Expand All @@ -149,6 +152,23 @@ export default function Message(props: Props) {
}}
>
<Grid container wrap="nowrap" spacing={1.5}>
<Grid item>
<Checkbox
checked={isSelected}
onChange={(e) => {
if (!props.isSystem) {
props.onSelectMessage(props.msg.id, e.target.checked)
}
}}
icon={<CheckBoxOutlineBlankIcon />}
checkedIcon={<CheckBoxIcon />}
sx={{
visibility: props.isDeleteMode ? 'visible' : 'hidden',
opacity: props.isSystem ? 0.5 : 1,
pointerEvents: props.isSystem ? 'none' : 'auto',
}}
/>
</Grid>
<Grid item>
<Box sx={{ marginTop: '8px' }}>
{
Expand All @@ -169,7 +189,7 @@ export default function Message(props: Props) {
height: '28px',
}}
>
<SmartToyIcon fontSize='small' />
<SmartToyIcon fontSize="small" />
</Avatar>
),
user: (
Expand All @@ -178,52 +198,44 @@ export default function Message(props: Props) {
width: '28px',
height: '28px',
}}
className='cursor-pointer'
className="cursor-pointer"
onClick={() => setOpenSettingWindow('chat')}
>
<PersonIcon fontSize='small' />
<PersonIcon fontSize="small" />
</Avatar>
),
system: (
<Avatar
sx={{
backgroundColor: theme.palette.warning.main,
width: '28px',
height: '28px',
}}
>
<SettingsIcon fontSize="small" />
</Avatar>
),
system:
<Avatar
sx={{
backgroundColor: theme.palette.warning.main,
width: '28px',
height: '28px',
}}
>
<SettingsIcon fontSize='small' />
</Avatar>
}[msg.role]
}
</Box>
</Grid>
<Grid item xs sm container sx={{ width: '0px', paddingRight: '15px' }}>
<Grid item xs>
<Box className={cn('msg-content', { 'msg-content-small': small })} sx={
small ? { fontSize: theme.typography.body2.fontSize } : {}
}>
{
enableMarkdownRendering && !isCollapsed ? (
<Markdown>
{content}
</Markdown>
) : (
<div>
{content}
{
needCollapse && isCollapsed && (
CollapseButton
)
}
</div>
)
}
<Box
className={cn('msg-content', { 'msg-content-small': small })}
sx={small ? { fontSize: theme.typography.body2.fontSize } : {}}
>
{enableMarkdownRendering && !isCollapsed ? (
<Markdown>{content}</Markdown>
) : (
<div>
{content}
{needCollapse && isCollapsed && CollapseButton}
</div>
)}
</Box>
<MessageErrTips msg={msg} />
{
needCollapse && !isCollapsed && CollapseButton
}
{needCollapse && !isCollapsed && CollapseButton}
<Typography variant="body2" sx={{ opacity: 0.5, paddingBottom: '2rem' }}>
{tips.join(', ')}
</Typography>
Expand Down
40 changes: 22 additions & 18 deletions src/renderer/components/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,37 @@ import Message from './Message'
import * as atoms from '../stores/atoms'
import { useAtom, useAtomValue } from 'jotai'
import { cn } from '@/lib/utils'
import { useMessageSelectionContext } from '../contexts/MessageSelectionContext'

interface Props { }

export default function MessageList(props: Props) {
export default function MessageList() {
const currentSession = useAtomValue(atoms.currentSessionAtom)
const currentMessageList = useAtomValue(atoms.currentMessageListAtom)
const ref = useRef<HTMLDivElement | null>(null)
const [, setMessageListRef] = useAtom(atoms.messageListRefAtom)
const { isDeleteMode, selectedMessages, handleSelectMessage } = useMessageSelectionContext()

useEffect(() => {
setMessageListRef(ref)
}, [ref])

return (
<div className={cn('w-full h-3/4 mx-auto')}>
<div className='overflow-y-auto h-full pr-0 pl-0' ref={ref}>
{
currentMessageList.map((msg, index) => (
<Message
id={msg.id}
key={'msg-' + msg.id}
msg={msg}
sessionId={currentSession.id}
sessionType={currentSession.type || 'chat'}
className={index === 0 ? 'pt-4' : ''}
collapseThreshold={msg.role === 'system' ? 150 : undefined}
/>
))
}
<div className={cn('mx-auto w-full h-3/4')}>
<div className="overflow-y-auto pr-0 pl-0 h-full" ref={ref}>
{currentMessageList.map((msg, index) => (
<Message
id={msg.id}
key={'msg-' + msg.id}
msg={msg}
sessionId={currentSession.id}
sessionType={currentSession.type || 'chat'}
className={index === 0 ? 'pt-4' : ''}
collapseThreshold={msg.role === 'system' ? 150 : undefined}
isDeleteMode={isDeleteMode}
onSelectMessage={handleSelectMessage}
isSelected={selectedMessages.has(msg.id)}
isSystem={msg.role === 'system'}
/>
))}
</div>
</div>
)
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/contexts/MessageSelectionContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, { createContext, useContext } from 'react'
import { useMessageSelection } from '../hooks/useMessageSelection'

const MessageSelectionContext = createContext<ReturnType<typeof useMessageSelection> | null>(null)

export const MessageSelectionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const messageSelection = useMessageSelection()
return <MessageSelectionContext.Provider value={messageSelection}>{children}</MessageSelectionContext.Provider>
}

export const useMessageSelectionContext = () => {
const context = useContext(MessageSelectionContext)
if (!context) {
throw new Error('useMessageSelectionContext must be used within a MessageSelectionProvider')
}
return context
}
Loading