feat: change re-generage message logic

This commit is contained in:
kangfenmao 2024-12-03 19:58:27 +08:00
parent 368de84440
commit 73973ecb7f
12 changed files with 110 additions and 58 deletions

View File

@ -1,6 +1,6 @@
@font-face {
font-family: 'iconfont'; /* Project id 4563475 */
src: url('iconfont.woff2?t=1725606177995') format('woff2');
font-family: 'iconfont'; /* Project id 4753420 */
src: url('iconfont.woff2?t=1733224456443') format('woff2');
}
.iconfont {
@ -11,6 +11,14 @@
-moz-osx-font-smoothing: grayscale;
}
.icon-at1:before {
content: '\e7df';
}
.icon-at:before {
content: '\e630';
}
.icon-a-darkmode:before {
content: '\e6cd';
}
@ -27,10 +35,6 @@
content: '\e942';
}
.icon-grid-row-2copy:before {
content: '\e681';
}
.icon-inbox:before {
content: '\e869';
}
@ -71,10 +75,6 @@
content: '\e944';
}
.icon-a-addchat:before {
content: '\e658';
}
.icon-appstore:before {
content: '\e792';
}

View File

@ -49,6 +49,8 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
setTimeout(resizeTextArea, 0)
}, [])
TextEditPopup.hide = onCancel
return (
<Modal
title={t('common.edit')}
@ -75,10 +77,12 @@ const PopupContainer: React.FC<Props> = ({ text, textareaProps, modalProps, reso
)
}
const TopViewKey = 'TextEditPopup'
export default class TextEditPopup {
static topviewId = 0
static hide() {
TopView.hide('TextEditPopup')
TopView.hide(TopViewKey)
}
static show(props: ShowParams) {
return new Promise<any>((resolve) => {
@ -87,10 +91,10 @@ export default class TextEditPopup {
{...props}
resolve={(v) => {
resolve(v)
this.hide()
TopView.hide(TopViewKey)
}}
/>,
'TextEditPopup'
TopViewKey
)
})
}

View File

@ -240,7 +240,8 @@
"topic.added": "New topic added",
"upgrade.success.button": "Restart",
"upgrade.success.content": "Please restart the application to complete the upgrade",
"upgrade.success.title": "Upgrade successfully"
"upgrade.success.title": "Upgrade successfully",
"regenerate.confirm": "Regenerating will replace current message"
},
"minapp": {
"title": "MinApp"

View File

@ -240,7 +240,8 @@
"topic.added": "Новый топик добавлен",
"upgrade.success.button": "Перезапустить",
"upgrade.success.content": "Пожалуйста, перезапустите приложение для завершения обновления",
"upgrade.success.title": "Обновление успешно"
"upgrade.success.title": "Обновление успешно",
"regenerate.confirm": "Перегенерация заменит текущее сообщение"
},
"minapp": {
"title": "Встроенные приложения"

View File

@ -240,7 +240,8 @@
"topic.added": "话题添加成功",
"upgrade.success.button": "重启",
"upgrade.success.content": "重启用以完成升级",
"upgrade.success.title": "升级成功"
"upgrade.success.title": "升级成功",
"regenerate.confirm": "重新生成会覆盖当前消息"
},
"minapp": {
"title": "小程序"

View File

@ -240,7 +240,8 @@
"topic.added": "新話題已添加",
"upgrade.success.button": "重新啟動",
"upgrade.success.content": "請重新啟動應用以完成升級",
"upgrade.success.title": "升級成功"
"upgrade.success.title": "升級成功",
"regenerate.confirm": "重新生成會覆蓋當前訊息"
},
"minapp": {
"title": "小程序"

View File

@ -272,8 +272,8 @@ const Tabs = styled(TabsAntd)<{ $language: string }>`
padding-right: 0 !important;
}
.ant-tabs-nav {
min-width: ${({ $language }) => ($language.startsWith('zh') ? '100px' : '140px')};
max-width: ${({ $language }) => ($language.startsWith('zh') ? '100px' : '140px')};
min-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
max-width: ${({ $language }) => ($language.startsWith('zh') ? '110px' : '140px')};
}
.ant-tabs-nav-list {
padding: 10px 8px;

View File

@ -30,6 +30,9 @@ interface Props {
onDeleteMessage?: (message: Message) => void
}
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
isBubbleStyle ? (isAssistantMessage ? 'var(--chat-background-assistant)' : 'var(--chat-background-user)') : undefined
const MessageItem: FC<Props> = ({
message: _message,
topic,
@ -57,37 +60,33 @@ const MessageItem: FC<Props> = ({
}, [messageFont])
const messageBorder = showMessageDivider ? undefined : 'none'
const messageBackground = isBubbleStyle
? isAssistantMessage
? 'var(--chat-background-assistant)'
: 'var(--chat-background-user)'
: undefined
const messageBackground = getMessageBackground(isBubbleStyle, isAssistantMessage)
const onEditMessage = useCallback(
(msg: Message) => {
setMessage(msg)
const messages = onGetMessages?.().map((m) => (m.id === message.id ? msg : m))
const messages = onGetMessages?.()?.map((m) => (m.id === message.id ? msg : m))
messages && onSetMessages?.(messages)
topic && db.topics.update(topic.id, { messages })
},
[message, onGetMessages, onSetMessages, topic]
[message.id, onGetMessages, onSetMessages, topic]
)
const messageHighlightHandler = (highlight: boolean = true) => {
if (messageContainerRef.current) {
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
if (highlight) {
setTimeout(() => {
const classList = messageContainerRef.current?.classList
classList?.add('message-highlight')
setTimeout(() => classList?.remove('message-highlight'), 2500)
}, 500)
}
}
}
useEffect(() => {
const unsubscribes = [
EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, (highlight: boolean = true) => {
if (messageContainerRef.current) {
messageContainerRef.current.scrollIntoView({ behavior: 'smooth' })
if (highlight) {
setTimeout(() => {
const classList = messageContainerRef.current?.classList
classList?.add('message-highlight')
setTimeout(() => classList?.remove('message-highlight'), 2500)
}, 500)
}
}
})
]
const unsubscribes = [EventEmitter.on(EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id, messageHighlightHandler)]
return () => unsubscribes.forEach((unsub) => unsub())
}, [message])
@ -105,11 +104,16 @@ const MessageItem: FC<Props> = ({
useEffect(() => {
if (topic && onGetMessages && onSetMessages) {
if (message.status === 'sending' && index === 0) {
if (message.status === 'sending') {
const messages = onGetMessages()
fetchChatCompletion({
message,
messages: messages.filter((m) => !m.status.includes('ing')),
messages: messages
.filter((m) => !m.status.includes('ing'))
.slice(
0,
messages.findIndex((m) => m.id === message.id)
),
assistant,
topic,
onResponse: (msg) => {
@ -124,7 +128,7 @@ const MessageItem: FC<Props> = ({
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
}, [message.status])
if (hidePresetMessages && message.isPreset) {
return null
@ -148,7 +152,7 @@ const MessageItem: FC<Props> = ({
})}
ref={messageContainerRef}
style={isBubbleStyle ? { alignItems: isAssistantMessage ? 'start' : 'end' } : undefined}>
<MessageHeader message={message} assistant={assistant} model={model} />
<MessageHeader message={message} assistant={assistant} model={model} key={message.modelId} />
<MessageContentContainer
className="message-content-container"
style={{ fontFamily, fontSize, background: messageBackground }}>
@ -164,6 +168,7 @@ const MessageItem: FC<Props> = ({
<MessageTokens message={message} isLastMessage={isLastMessage} />
<MessageMenubar
message={message}
assistantModel={assistant.model}
model={model}
index={index}
isLastMessage={isLastMessage}

View File

@ -9,7 +9,7 @@ import { Assistant, Message, Model } from '@renderer/types'
import { firstLetter, removeLeadingEmoji } from '@renderer/utils'
import { Avatar } from 'antd'
import dayjs from 'dayjs'
import { CSSProperties, FC, useCallback, useMemo } from 'react'
import { CSSProperties, FC, memo, useCallback, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -19,18 +19,19 @@ interface Props {
model?: Model
}
const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
if (isLocalAi) return AppLogo
return modelId ? getModelLogo(modelId) : undefined
}
const MessageHeader: FC<Props> = memo(({ assistant, model, message }) => {
const avatar = useAvatar()
const { theme } = useTheme()
const { userName } = useSettings()
const { t } = useTranslation()
const { isBubbleStyle } = useMessageStyle()
const avatarSource = useMemo(() => {
if (isLocalAi) return AppLogo
return message.modelId ? getModelLogo(message.modelId) : undefined
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [message.modelId, theme])
const avatarSource = useMemo(() => getAvatarSource(isLocalAi, message.modelId), [message.modelId])
const getUserName = useCallback(() => {
if (isLocalAi && message.role !== 'user') return APP_NAME
@ -43,7 +44,7 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
const avatarName = useMemo(() => firstLetter(assistant?.name).toUpperCase(), [assistant?.name])
const username = useMemo(() => removeLeadingEmoji(getUserName()), [getUserName])
const showMiniApp = () => model?.provider && startMinAppById(model?.provider)
const showMiniApp = useCallback(() => model?.provider && startMinAppById(model.provider), [model?.provider])
const avatarStyle: CSSProperties | undefined = isBubbleStyle
? {
@ -83,7 +84,9 @@ const MessageHeader: FC<Props> = ({ assistant, model, message }) => {
</AvatarWrapper>
</Container>
)
}
})
MessageHeader.displayName = 'MessageHeader'
const Container = styled.div`
display: flex;

View File

@ -23,6 +23,7 @@ import styled from 'styled-components'
interface Props {
message: Message
assistantModel?: Model
model?: Model
index?: number
isLastMessage: boolean
@ -33,7 +34,17 @@ interface Props {
}
const MessageMenubar: FC<Props> = (props) => {
const { message, index, model, isLastMessage, isAssistantMessage, setModel, onEditMessage, onDeleteMessage } = props
const {
message,
index,
model,
isLastMessage,
isAssistantMessage,
assistantModel,
setModel,
onEditMessage,
onDeleteMessage
} = props
const { t } = useTranslation()
const [copied, setCopied] = useState(false)
const [isTranslating, setIsTranslating] = useState(false)
@ -157,11 +168,21 @@ const MessageMenubar: FC<Props> = (props) => {
[handleTranslate, isTranslating, message, onEdit, onEditMessage, t]
)
const onSelectModel = async () => {
const onAtModelRegenerate = async () => {
const selectedModel = await SelectModelPopup.show({ model })
selectedModel && onRegenerate(selectedModel)
}
const onDeleteAndRegenerate = () => {
onEditMessage?.({
...message,
content: '',
status: 'sending',
modelId: assistantModel?.id || model?.id,
translatedContent: undefined
})
}
return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && (
@ -177,10 +198,22 @@ const MessageMenubar: FC<Props> = (props) => {
{copied && <CheckOutlined style={{ color: 'var(--color-primary)' }} />}
</ActionButton>
</Tooltip>
{isAssistantMessage && (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
destroyTooltipOnHide
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={onDeleteAndRegenerate}>
<ActionButton className="message-action-button">
<SyncOutlined />
</ActionButton>
</Popconfirm>
)}
{canRegenerate && (
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onSelectModel}>
<SyncOutlined />
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
<i className="iconfont icon-at1"></i>
</ActionButton>
</Tooltip>
)}
@ -247,6 +280,9 @@ const ActionButton = styled.div`
&:hover {
color: var(--color-text-1);
}
.icon-at1 {
font-size: 16px;
}
`
export default MessageMenubar

View File

@ -55,7 +55,7 @@ const initialState: SettingsState = {
theme: ThemeMode.auto,
windowStyle: 'transparent',
fontSize: 14,
topicPosition: 'right',
topicPosition: 'left',
showTopicTime: false,
pasteLongTextAsFile: false,
clickAssistantToShowTopic: false,