mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-29 23:12:38 +08:00
feat: edit message
This commit is contained in:
parent
29ee364fc5
commit
d7db307f8a
98
src/renderer/src/components/Popups/TextEditPopup.tsx
Normal file
98
src/renderer/src/components/Popups/TextEditPopup.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import { Modal, ModalProps } from 'antd'
|
||||||
|
import TextArea from 'antd/es/input/TextArea'
|
||||||
|
import { TextAreaProps } from 'antd/lib/input'
|
||||||
|
import { TextAreaRef } from 'antd/lib/input/TextArea'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
import { TopView } from '../TopView'
|
||||||
|
|
||||||
|
interface ShowParams {
|
||||||
|
text: string
|
||||||
|
textareaProps?: TextAreaProps
|
||||||
|
modalProps?: ModalProps
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props extends ShowParams {
|
||||||
|
resolve: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, resolve }) => {
|
||||||
|
const [open, setOpen] = useState(true)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [textValue, setTextValue] = useState(text)
|
||||||
|
const textareaRef = useRef<TextAreaRef>(null)
|
||||||
|
|
||||||
|
const onOk = () => {
|
||||||
|
setOpen(false)
|
||||||
|
resolve(textValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
setOpen(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClose = () => {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resizeTextArea = () => {
|
||||||
|
const textArea = textareaRef.current?.resizableTextArea?.textArea
|
||||||
|
const maxHeight = innerHeight * 0.6
|
||||||
|
if (textArea) {
|
||||||
|
textArea.style.height = 'auto'
|
||||||
|
textArea.style.height = textArea?.scrollHeight > maxHeight ? maxHeight + 'px' : `${textArea?.scrollHeight}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(resizeTextArea, 0)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={t('common.edit')}
|
||||||
|
width="60vw"
|
||||||
|
style={{ maxHeight: '70vh' }}
|
||||||
|
transitionName="ant-move-down"
|
||||||
|
maskTransitionName="ant-fade"
|
||||||
|
okText={t('common.save')}
|
||||||
|
{...modalProps}
|
||||||
|
open={open}
|
||||||
|
onOk={onOk}
|
||||||
|
onCancel={onCancel}
|
||||||
|
afterClose={onClose}
|
||||||
|
centered>
|
||||||
|
<TextArea
|
||||||
|
ref={textareaRef}
|
||||||
|
rows={4}
|
||||||
|
autoFocus
|
||||||
|
{...textareaProps}
|
||||||
|
value={textValue}
|
||||||
|
onInput={resizeTextArea}
|
||||||
|
onChange={(e) => setTextValue(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TextEditPopup {
|
||||||
|
static topviewId = 0
|
||||||
|
static hide() {
|
||||||
|
TopView.hide('TextEditPopup')
|
||||||
|
}
|
||||||
|
static show(props: ShowParams) {
|
||||||
|
return new Promise<any>((resolve) => {
|
||||||
|
TopView.show(
|
||||||
|
<PopupContainer
|
||||||
|
{...props}
|
||||||
|
resolve={(v) => {
|
||||||
|
resolve(v)
|
||||||
|
this.hide()
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
'TextEditPopup'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -309,6 +309,7 @@ const resources = {
|
|||||||
regenerate: '重新生成',
|
regenerate: '重新生成',
|
||||||
provider: '提供商',
|
provider: '提供商',
|
||||||
you: '用户',
|
you: '用户',
|
||||||
|
save: '保存',
|
||||||
footnote: '引用内容',
|
footnote: '引用内容',
|
||||||
select: '选择',
|
select: '选择',
|
||||||
search: '搜索',
|
search: '搜索',
|
||||||
|
|||||||
@ -28,10 +28,11 @@ interface Props {
|
|||||||
index?: number
|
index?: number
|
||||||
total?: number
|
total?: number
|
||||||
lastMessage?: boolean
|
lastMessage?: boolean
|
||||||
|
onEditMessage?: (message: Message) => void
|
||||||
onDeleteMessage?: (message: Message) => void
|
onDeleteMessage?: (message: Message) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }) => {
|
const MessageItem: FC<Props> = ({ message, index, lastMessage, onEditMessage, onDeleteMessage }) => {
|
||||||
const avatar = useAvatar()
|
const avatar = useAvatar()
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||||
@ -117,6 +118,7 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }
|
|||||||
isLastMessage={isLastMessage}
|
isLastMessage={isLastMessage}
|
||||||
isAssistantMessage={isAssistantMessage}
|
isAssistantMessage={isAssistantMessage}
|
||||||
setModel={setModel}
|
setModel={setModel}
|
||||||
|
onEditMessage={onEditMessage}
|
||||||
onDeleteMessage={onDeleteMessage}
|
onDeleteMessage={onDeleteMessage}
|
||||||
/>
|
/>
|
||||||
</MessageFooter>
|
</MessageFooter>
|
||||||
@ -126,7 +128,10 @@ const MessageItem: FC<Props> = ({ message, index, lastMessage, onDeleteMessage }
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageContent: React.FC<{ message: Message; model?: Model }> = ({ message, model }) => {
|
const MessageContent: React.FC<{
|
||||||
|
message: Message
|
||||||
|
model?: Model
|
||||||
|
}> = ({ message, model }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
if (message.status === 'sending') {
|
if (message.status === 'sending') {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import {
|
|||||||
SaveOutlined,
|
SaveOutlined,
|
||||||
SyncOutlined
|
SyncOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
|
import TextEditPopup from '@renderer/components/Popups/TextEditPopup'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/event'
|
||||||
import { Message, Model } from '@renderer/types'
|
import { Message, Model } from '@renderer/types'
|
||||||
import { removeTrailingDoubleSpaces } from '@renderer/utils'
|
import { removeTrailingDoubleSpaces } from '@renderer/utils'
|
||||||
@ -25,19 +26,18 @@ interface Props {
|
|||||||
isLastMessage: boolean
|
isLastMessage: boolean
|
||||||
isAssistantMessage: boolean
|
isAssistantMessage: boolean
|
||||||
setModel: (model: Model) => void
|
setModel: (model: Model) => void
|
||||||
|
onEditMessage?: (message: Message) => void
|
||||||
onDeleteMessage?: (message: Message) => void
|
onDeleteMessage?: (message: Message) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const MessageMenubar: FC<Props> = (props) => {
|
const MessageMenubar: FC<Props> = (props) => {
|
||||||
const { message, index, model, isLastMessage, isAssistantMessage, setModel, onDeleteMessage } = props
|
const { message, index, model, isLastMessage, isAssistantMessage, setModel, onEditMessage, onDeleteMessage } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
const isUserMessage = message.role === 'user'
|
const isUserMessage = message.role === 'user'
|
||||||
const canRegenerate = isLastMessage && isAssistantMessage
|
const canRegenerate = isLastMessage && isAssistantMessage
|
||||||
|
|
||||||
const onEdit = useCallback(() => EventEmitter.emit(EVENT_NAMES.EDIT_MESSAGE, message), [message])
|
|
||||||
|
|
||||||
const onCopy = useCallback(() => {
|
const onCopy = useCallback(() => {
|
||||||
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
|
||||||
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
window.message.success({ content: t('message.copied'), key: 'copy-message' })
|
||||||
@ -57,6 +57,11 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||||
}, [index])
|
}, [index])
|
||||||
|
|
||||||
|
const onEdit = useCallback(async () => {
|
||||||
|
const editedText = await TextEditPopup.show({ text: message.content })
|
||||||
|
editedText && onEditMessage?.({ ...message, content: editedText })
|
||||||
|
}, [message, onEditMessage])
|
||||||
|
|
||||||
const dropdownItems = useMemo(
|
const dropdownItems = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
@ -67,9 +72,15 @@ const MessageMenubar: FC<Props> = (props) => {
|
|||||||
const fileName = message.createdAt + '.md'
|
const fileName = message.createdAt + '.md'
|
||||||
window.api.file.save(fileName, message.content)
|
window.api.file.save(fileName, message.content)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('common.edit'),
|
||||||
|
key: 'edit',
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
onClick: onEdit
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[t, message]
|
[message.content, message.createdAt, onEdit, t]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -69,6 +69,15 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
[messages, topic.id]
|
[messages, topic.id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const onEditMessage = useCallback(
|
||||||
|
(message: Message) => {
|
||||||
|
const _messages = messages.map((m) => (m.id === message.id ? message : m))
|
||||||
|
setMessages(_messages)
|
||||||
|
db.topics.update(topic.id, { messages: _messages })
|
||||||
|
},
|
||||||
|
[messages, topic.id]
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
|
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, async (msg: Message) => {
|
||||||
@ -199,7 +208,13 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
|
|||||||
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
<Suggestions assistant={assistant} messages={messages} lastMessage={lastMessage} />
|
||||||
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} lastMessage />}
|
{lastMessage && <MessageItem key={lastMessage.id} message={lastMessage} lastMessage />}
|
||||||
{reverse([...messages]).map((message, index) => (
|
{reverse([...messages]).map((message, index) => (
|
||||||
<MessageItem key={message.id} message={message} index={index} onDeleteMessage={onDeleteMessage} />
|
<MessageItem
|
||||||
|
key={message.id}
|
||||||
|
message={message}
|
||||||
|
index={index}
|
||||||
|
onEditMessage={onEditMessage}
|
||||||
|
onDeleteMessage={onDeleteMessage}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
<Prompt assistant={assistant} key={assistant.prompt} />
|
<Prompt assistant={assistant} key={assistant.prompt} />
|
||||||
</Container>
|
</Container>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user