mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-10 23:59:45 +08:00
Remove loading state blocks input, Add "Retry failed messages" button (#9513)
* feat(chat/input): allow sending during streaming; remove loading guard - Inputbar: drop loading check; keep Send clickable; right-click to pause - Message/MessageMenubar: render menubar even while streaming - Minor cleanup of unused imports and lints * feat(chat/menubar): one-click regenerate (remove confirm); add empty-context fallback - MessageMenubar: remove Popconfirm; click to regenerate - ApiService: ensure last user message is included if filters empty (avoid messages=[]) - Minor cleanup * feat(chat/ui): decouple generation from sending; enable actions during streaming - MessageGroupMenuBar: add “Retry all” (ReloadOutlined) to retry errored/empty/no-block replies; tooltip via i18n - Context fallback: always include last user message in both messageThunk and ApiService - i18n: add message.group.retry_failed (zh-CN, en-US, ja-JP, ru-RU, zh-TW) - Cleanup: remove unused imports; fix lints * feat(chat/settings): Add confirmation settings for message actions - Add "Confirm before deleting message" and "Confirm before regenerating message" options to the settings page, allowing users to customize action confirmations. - Update internationalization files to support multi-language prompt messages. - Modify the message menu bar to integrate the confirmation logic, enhancing the user experience. * fix(chat/ui): Apply regeneration confirmation to user messages Previously, the "Regenerate" action on user messages would trigger immediately, bypassing the `confirmRegenerateMessage` setting. This behavior was inconsistent with the regeneration logic for assistant messages, which correctly showed a confirmation dialog. This commit wraps the user message's regenerate button in a `Popconfirm` component, conditioned on the `confirmRegenerateMessage` setting. This aligns its behavior with the existing logic for assistant messages. Now, all regeneration actions are uniformly governed by the user's confirmation preference, creating a more consistent and predictable user experience. * fix(ui): Only show 'Retry failed' button when errors exist fix(ui): Conditionally render the 'Retry failed' button The 'Retry failed messages' button in the message group menu bar was previously always visible, even when no messages had failed. - The 'Retry failed' button is now conditionally rendered and will only appear if one or more messages in the group meet the failure criteria. * feat(chat/ui): Add dedicated button to pause message generation Replaced the undiscoverable right-click-to-pause functionality on the send button with a dedicated, visible "Pause" button. This new button only appears during message generation, making the action intuitive and accessible. - Removed `onPause` and context menu logic from `SendMessageButton`. - Added a conditional `CirclePause` button to the `Inputbar` when loading. * feat(settings/migrate): initialize confirm message flags for legacy users - Add migration 138 to default confirmDeleteMessage/confirmRegenerateMessage - No behavior change for fresh installs (uses initialState) fix(format): correct indentation in MessageMenubar fix(settings/migrate): fix persistedReducer verison * fix(ui): resolve React Hook dependency warnings - Remove unnecessary `topic.prompt` dependencies from Message components - Remove `loading` dependency from Inputbar useCallback Resolves ESLint exhaustive-deps warnings and conflicts --------- Co-authored-by: n2yt584v2t4nh7y <117180266+n2yt584v2t4nh7y@users.noreply.github.com>
This commit is contained in:
parent
16b9f49cc8
commit
2480822690
@ -1320,7 +1320,8 @@
|
||||
"delete": {
|
||||
"content": "Deleting a group message will delete the user's question and all assistant's answers",
|
||||
"title": "Delete Group Message"
|
||||
}
|
||||
},
|
||||
"retry_failed": "Retry failed messages"
|
||||
},
|
||||
"ignore": {
|
||||
"knowledge": {
|
||||
@ -3347,6 +3348,8 @@
|
||||
"label": "Grid detail trigger"
|
||||
},
|
||||
"input": {
|
||||
"confirm_delete_message": "Confirm before deleting messages",
|
||||
"confirm_regenerate_message": "Confirm before regenerating messages",
|
||||
"enable_quick_triggers": "Enable / and @ triggers",
|
||||
"paste_long_text_as_file": "Paste long text as file",
|
||||
"paste_long_text_threshold": "Paste long text length",
|
||||
|
||||
@ -1320,7 +1320,8 @@
|
||||
"delete": {
|
||||
"content": "分組メッセージを削除するとユーザーの質問と助け手の回答がすべて削除されます",
|
||||
"title": "分組メッセージを削除"
|
||||
}
|
||||
},
|
||||
"retry_failed": "エラーになったメッセージを再試行"
|
||||
},
|
||||
"ignore": {
|
||||
"knowledge": {
|
||||
@ -3347,6 +3348,8 @@
|
||||
"label": "グリッド詳細トリガー"
|
||||
},
|
||||
"input": {
|
||||
"confirm_delete_message": "メッセージ削除前に確認",
|
||||
"confirm_regenerate_message": "メッセージ再生成前に確認",
|
||||
"enable_quick_triggers": "/ と @ を有効にしてクイックメニューを表示します。",
|
||||
"paste_long_text_as_file": "長いテキストをファイルとして貼り付け",
|
||||
"paste_long_text_threshold": "長いテキストの長さ",
|
||||
|
||||
@ -1320,7 +1320,8 @@
|
||||
"delete": {
|
||||
"content": "Удаление группы сообщений удалит пользовательский вопрос и все ответы помощника",
|
||||
"title": "Удалить группу сообщений"
|
||||
}
|
||||
},
|
||||
"retry_failed": "Повторить неудавшиеся сообщения"
|
||||
},
|
||||
"ignore": {
|
||||
"knowledge": {
|
||||
@ -3347,6 +3348,8 @@
|
||||
"label": "Триггер для отображения подробной информации в сетке"
|
||||
},
|
||||
"input": {
|
||||
"confirm_delete_message": "Подтверждать перед удалением сообщений",
|
||||
"confirm_regenerate_message": "Подтверждать перед пересозданием сообщений",
|
||||
"enable_quick_triggers": "Включите / и @, чтобы вызвать быстрое меню.",
|
||||
"paste_long_text_as_file": "Вставлять длинный текст как файл",
|
||||
"paste_long_text_threshold": "Длина вставки длинного текста",
|
||||
|
||||
@ -1320,7 +1320,8 @@
|
||||
"delete": {
|
||||
"content": "删除分组消息会删除用户提问和所有助手的回答",
|
||||
"title": "删除分组消息"
|
||||
}
|
||||
},
|
||||
"retry_failed": "重试出错的消息"
|
||||
},
|
||||
"ignore": {
|
||||
"knowledge": {
|
||||
@ -3347,6 +3348,8 @@
|
||||
"label": "网格详情触发"
|
||||
},
|
||||
"input": {
|
||||
"confirm_delete_message": "删除消息前确认",
|
||||
"confirm_regenerate_message": "重新生成消息前确认",
|
||||
"enable_quick_triggers": "启用 / 和 @ 触发快捷菜单",
|
||||
"paste_long_text_as_file": "长文本粘贴为文件",
|
||||
"paste_long_text_threshold": "长文本长度",
|
||||
|
||||
@ -1320,7 +1320,8 @@
|
||||
"delete": {
|
||||
"content": "刪除分組訊息會刪除使用者提問和所有助手的回答",
|
||||
"title": "刪除分組訊息"
|
||||
}
|
||||
},
|
||||
"retry_failed": "重試出錯的訊息"
|
||||
},
|
||||
"ignore": {
|
||||
"knowledge": {
|
||||
@ -3347,6 +3348,8 @@
|
||||
"label": "網格詳細資訊觸發"
|
||||
},
|
||||
"input": {
|
||||
"confirm_delete_message": "刪除訊息前確認",
|
||||
"confirm_regenerate_message": "重新生成訊息前確認",
|
||||
"enable_quick_triggers": "啟用 / 和 @ 觸發快捷選單",
|
||||
"paste_long_text_as_file": "將長文字貼上為檔案",
|
||||
"paste_long_text_threshold": "長文字長度",
|
||||
|
||||
@ -210,7 +210,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
)
|
||||
|
||||
const sendMessage = useCallback(async () => {
|
||||
if (inputEmpty || loading) {
|
||||
if (inputEmpty) {
|
||||
return
|
||||
}
|
||||
if (checkRateLimit(assistant)) {
|
||||
@ -258,7 +258,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
logger.warn('Failed to send message:', error as Error)
|
||||
parent?.recordException(error as Error)
|
||||
}
|
||||
}, [assistant, dispatch, files, inputEmpty, loading, mentionedModels, resizeTextArea, setTimeoutTimer, text, topic])
|
||||
}, [assistant, dispatch, files, inputEmpty, mentionedModels, resizeTextArea, setTimeoutTimer, text, topic])
|
||||
|
||||
const translate = useCallback(async () => {
|
||||
if (isTranslating) {
|
||||
@ -927,6 +927,7 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
onClick={onNewContext}
|
||||
/>
|
||||
<TranslateButton text={text} onTranslated={onTranslated} isLoading={isTranslating} />
|
||||
<SendMessageButton sendMessage={sendMessage} disabled={inputEmpty} />
|
||||
{loading && (
|
||||
<Tooltip placement="top" title={t('chat.input.pause')} mouseLeaveDelay={0} arrow>
|
||||
<ToolbarButton type="text" onClick={onPause} style={{ marginRight: -2 }}>
|
||||
@ -934,7 +935,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic, topic }) =
|
||||
</ToolbarButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{!loading && <SendMessageButton sendMessage={sendMessage} disabled={loading || inputEmpty} />}
|
||||
</ToolbarMenu>
|
||||
</Toolbar>
|
||||
</InputBarContainer>
|
||||
|
||||
@ -60,7 +60,6 @@ const MessageItem: FC<Props> = ({
|
||||
index,
|
||||
hideMenuBar = false,
|
||||
isGrouped,
|
||||
isStreaming = false,
|
||||
onUpdateUseful,
|
||||
isGroupContextMessage
|
||||
}) => {
|
||||
@ -116,7 +115,7 @@ const MessageItem: FC<Props> = ({
|
||||
|
||||
const isLastMessage = index === 0 || !!isGrouped
|
||||
const isAssistantMessage = message.role === 'assistant'
|
||||
const showMenubar = !hideMenuBar && !isStreaming && !message.status.includes('ing') && !isEditing
|
||||
const showMenubar = !hideMenuBar && !isEditing
|
||||
|
||||
const messageHighlightHandler = useCallback(
|
||||
(highlight: boolean = true) => {
|
||||
|
||||
@ -3,13 +3,17 @@ import {
|
||||
ColumnWidthOutlined,
|
||||
DeleteOutlined,
|
||||
FolderOutlined,
|
||||
NumberOutlined
|
||||
NumberOutlined,
|
||||
ReloadOutlined
|
||||
} from '@ant-design/icons'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import { useAssistant } from '@renderer/hooks/useAssistant'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import type { Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { AssistantMessageStatus } from '@renderer/types/newMessage'
|
||||
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||
import { Button, Tooltip } from 'antd'
|
||||
import { FC, memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -36,7 +40,8 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
topic
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { deleteGroupMessages } = useMessageOperations(topic)
|
||||
const { deleteGroupMessages, regenerateAssistantMessage } = useMessageOperations(topic)
|
||||
const { assistant } = useAssistant(messages[0]?.assistantId)
|
||||
|
||||
const handleDeleteGroup = async () => {
|
||||
const askId = messages[0]?.askId
|
||||
@ -54,6 +59,39 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
})
|
||||
}
|
||||
|
||||
const isFailedMessage = (m: Message) => {
|
||||
if (m.role !== 'assistant') return false
|
||||
const isError = (m.status || '').toLowerCase() === 'error'
|
||||
const content = getMainTextContent(m)
|
||||
const noContent = !content || content.trim().length === 0
|
||||
const noBlocks = !m.blocks || m.blocks.length === 0
|
||||
return isError || noContent || noBlocks
|
||||
}
|
||||
|
||||
const isTransmittingMessage = (m: Message) => {
|
||||
if (m.role !== 'assistant') return false
|
||||
const status = m.status as AssistantMessageStatus
|
||||
return (
|
||||
status === AssistantMessageStatus.PROCESSING ||
|
||||
status === AssistantMessageStatus.PENDING ||
|
||||
status === AssistantMessageStatus.SEARCHING
|
||||
)
|
||||
}
|
||||
|
||||
const hasFailedMessages = messages.some((m) => isFailedMessage(m) && !isTransmittingMessage(m))
|
||||
|
||||
const handleRetryAll = async () => {
|
||||
const candidates = messages.filter((m) => isFailedMessage(m) && !isTransmittingMessage(m))
|
||||
|
||||
for (const msg of candidates) {
|
||||
try {
|
||||
await regenerateAssistantMessage(msg, assistant)
|
||||
} catch (e) {
|
||||
// swallow per-item errors to continue others
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const multiModelMessageStyleTextByLayout = {
|
||||
fold: t('message.message.multi_model_style.fold.label'),
|
||||
vertical: t('message.message.multi_model_style.vertical'),
|
||||
@ -95,6 +133,17 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
)}
|
||||
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
||||
</HStack>
|
||||
{hasFailedMessages && (
|
||||
<Tooltip title={t('message.group.retry_failed')} mouseEnterDelay={0.6}>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleRetryAll}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
// import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { loggerService } from '@logger'
|
||||
import { CopyIcon, DeleteIcon, EditIcon, RefreshIcon } from '@renderer/components/Icons'
|
||||
import ObsidianExportPopup from '@renderer/components/Popups/ObsidianExportPopup'
|
||||
@ -7,9 +7,9 @@ import SelectModelPopup from '@renderer/components/Popups/SelectModelPopup'
|
||||
import { isEmbeddingModel, isRerankModel, isVisionModel } from '@renderer/config/models'
|
||||
import { useMessageEditing } from '@renderer/context/MessageEditingContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
import { useMessageOperations, useTopicLoading } from '@renderer/hooks/useMessageOperations'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { useNotesSettings } from '@renderer/hooks/useNotesSettings'
|
||||
import { useEnableDeveloperMode, useMessageStyle } from '@renderer/hooks/useSettings'
|
||||
import { useEnableDeveloperMode, useMessageStyle, useSettings } from '@renderer/hooks/useSettings'
|
||||
import { useTemporaryValue } from '@renderer/hooks/useTemporaryValue'
|
||||
import useTranslate from '@renderer/hooks/useTranslate'
|
||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||
@ -84,7 +84,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const { toggleMultiSelectMode } = useChatContext(props.topic)
|
||||
const [copied, setCopied] = useTemporaryValue(false, 2000)
|
||||
const [isTranslating, setIsTranslating] = useState(false)
|
||||
const [showRegenerateTooltip, setShowRegenerateTooltip] = useState(false)
|
||||
// remove confirm for regenerate; tooltip stays simple
|
||||
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
|
||||
const { translateLanguages } = useTranslate()
|
||||
// const assistantModel = assistant?.model
|
||||
@ -99,8 +99,9 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
|
||||
const { isBubbleStyle } = useMessageStyle()
|
||||
const { enableDeveloperMode } = useEnableDeveloperMode()
|
||||
const { confirmDeleteMessage, confirmRegenerateMessage } = useSettings()
|
||||
|
||||
const loading = useTopicLoading(topic)
|
||||
// const loading = useTopicLoading(topic)
|
||||
|
||||
const isUserMessage = message.role === 'user'
|
||||
|
||||
@ -145,18 +146,15 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
)
|
||||
|
||||
const onNewBranch = useCallback(async () => {
|
||||
if (loading) return
|
||||
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
|
||||
window.message.success({ content: t('chat.message.new.branch.created'), key: 'new-branch' })
|
||||
}, [index, t, loading])
|
||||
}, [index, t])
|
||||
|
||||
const handleResendUserMessage = useCallback(
|
||||
async (messageUpdate?: Message) => {
|
||||
if (!loading) {
|
||||
await resendMessage(messageUpdate ?? message, assistant)
|
||||
}
|
||||
await resendMessage(messageUpdate ?? message, assistant)
|
||||
},
|
||||
[assistant, loading, message, resendMessage]
|
||||
[assistant, message, resendMessage]
|
||||
)
|
||||
|
||||
const { startEditing } = useMessageEditing()
|
||||
@ -392,7 +390,6 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
|
||||
const onRegenerate = async (e: React.MouseEvent | undefined) => {
|
||||
e?.stopPropagation?.()
|
||||
if (loading) return
|
||||
// No need to reset or edit the message anymore
|
||||
// const selectedModel = isGrouped ? model : assistantModel
|
||||
// const _message = resetAssistantMessage(message, selectedModel)
|
||||
@ -438,12 +435,11 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const onMentionModel = useCallback(
|
||||
async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
if (loading) return
|
||||
const selectedModel = await SelectModelPopup.show({ model, filter: mentionModelFilter })
|
||||
if (!selectedModel) return
|
||||
appendAssistantResponse(message, selectedModel, { ...assistant, model: selectedModel })
|
||||
},
|
||||
[appendAssistantResponse, assistant, loading, mentionModelFilter, message, model]
|
||||
[appendAssistantResponse, assistant, mentionModelFilter, message, model]
|
||||
)
|
||||
|
||||
const onUseful = useCallback(
|
||||
@ -469,16 +465,32 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{showMessageTokens && <MessageTokens message={message} />}
|
||||
<MenusBar
|
||||
className={classNames({ menubar: true, show: isLastMessage, 'user-bubble-style': isUserBubbleStyleMessage })}>
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={() => handleResendUserMessage()}
|
||||
$softHoverBg={isBubbleStyle}>
|
||||
<RefreshIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{message.role === 'user' &&
|
||||
(confirmRegenerateMessage ? (
|
||||
<Popconfirm
|
||||
title={t('message.regenerate.confirm')}
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => handleResendUserMessage()}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
$softHoverBg={isBubbleStyle}>
|
||||
<RefreshIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={() => handleResendUserMessage()}
|
||||
$softHoverBg={isBubbleStyle}>
|
||||
<RefreshIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
))}
|
||||
{message.role === 'user' && (
|
||||
<Tooltip title={t('common.edit')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onEdit} $softHoverBg={softHoverBg}>
|
||||
@ -492,24 +504,29 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
{copied && <Check size={15} color="var(--color-primary)" />}
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
{isAssistantMessage && (
|
||||
<Popconfirm
|
||||
title={t('message.regenerate.confirm')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<InfoCircleOutlined style={{ color: 'red' }} />}
|
||||
onConfirm={onRegenerate}
|
||||
onOpenChange={(open) => open && setShowRegenerateTooltip(false)}>
|
||||
<Tooltip
|
||||
title={t('common.regenerate')}
|
||||
mouseEnterDelay={0.8}
|
||||
open={showRegenerateTooltip}
|
||||
onOpenChange={setShowRegenerateTooltip}>
|
||||
<ActionButton className="message-action-button" $softHoverBg={softHoverBg}>
|
||||
{isAssistantMessage &&
|
||||
(confirmRegenerateMessage ? (
|
||||
<Popconfirm
|
||||
title={t('message.regenerate.confirm')}
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={onRegenerate}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
$softHoverBg={softHoverBg}>
|
||||
<RefreshIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onRegenerate} $softHoverBg={softHoverBg}>
|
||||
<RefreshIcon size={15} />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
</Popconfirm>
|
||||
)}
|
||||
))}
|
||||
{isAssistantMessage && (
|
||||
<Tooltip title={t('message.mention.title')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onMentionModel} $softHoverBg={softHoverBg}>
|
||||
@ -603,15 +620,32 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
icon={<InfoCircleOutlined style={{ color: 'red' }} />}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}
|
||||
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}>
|
||||
{confirmDeleteMessage ? (
|
||||
<Popconfirm
|
||||
title={t('message.message.delete.content')}
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => deleteMessage(message.id, message.traceId, message.model?.name)}
|
||||
onOpenChange={(open) => open && setShowDeleteTooltip(false)}>
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
$softHoverBg={softHoverBg}>
|
||||
<Tooltip
|
||||
title={t('common.delete')}
|
||||
mouseEnterDelay={1}
|
||||
open={showDeleteTooltip}
|
||||
onOpenChange={setShowDeleteTooltip}>
|
||||
<DeleteIcon size={15} />
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</Popconfirm>
|
||||
) : (
|
||||
<ActionButton
|
||||
className="message-action-button"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
deleteMessage(message.id, message.traceId, message.model?.name)
|
||||
}}
|
||||
$softHoverBg={softHoverBg}>
|
||||
<Tooltip
|
||||
title={t('common.delete')}
|
||||
@ -621,7 +655,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
<DeleteIcon size={15} />
|
||||
</Tooltip>
|
||||
</ActionButton>
|
||||
</Popconfirm>
|
||||
)}
|
||||
{enableDeveloperMode && message.traceId && (
|
||||
<Tooltip title={t('trace.label')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={() => handleTraceUserMessage()}>
|
||||
|
||||
@ -26,6 +26,8 @@ import {
|
||||
setCodeShowLineNumbers,
|
||||
setCodeViewer,
|
||||
setCodeWrappable,
|
||||
setConfirmDeleteMessage,
|
||||
setConfirmRegenerateMessage,
|
||||
setEnableQuickPanelTriggers,
|
||||
setFontSize,
|
||||
setMathEnableSingleDollar,
|
||||
@ -105,7 +107,9 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
messageNavigation,
|
||||
enableQuickPanelTriggers,
|
||||
showTranslateConfirm,
|
||||
showMessageOutline
|
||||
showMessageOutline,
|
||||
confirmDeleteMessage,
|
||||
confirmRegenerateMessage
|
||||
} = useSettings()
|
||||
|
||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||
@ -646,6 +650,24 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.input.confirm_delete_message')}</SettingRowTitleSmall>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={confirmDeleteMessage}
|
||||
onChange={(checked) => dispatch(setConfirmDeleteMessage(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.input.confirm_regenerate_message')}</SettingRowTitleSmall>
|
||||
<Switch
|
||||
size="small"
|
||||
checked={confirmRegenerateMessage}
|
||||
onChange={(checked) => dispatch(setConfirmRegenerateMessage(checked))}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.input.target_language.label')}</SettingRowTitleSmall>
|
||||
<Selector
|
||||
|
||||
@ -462,10 +462,15 @@ export async function fetchChatCompletion({
|
||||
|
||||
const filteredMessages4 = filterAdjacentUserMessaegs(filteredMessages3)
|
||||
|
||||
const _messages = filterUserRoleStartMessages(
|
||||
let _messages = filterUserRoleStartMessages(
|
||||
filterEmptyMessages(filterAfterContextClearMessages(takeRight(filteredMessages4, contextCount + 2))) // 取原来几个provider的最大值
|
||||
)
|
||||
|
||||
// Fallback: ensure at least the last user message is present to avoid empty payloads
|
||||
if ((!_messages || _messages.length === 0) && lastUserMessage) {
|
||||
_messages = [lastUserMessage]
|
||||
}
|
||||
|
||||
// FIXME: qwen3即使关闭思考仍然会导致enableReasoning的结果为true
|
||||
const enableReasoning =
|
||||
((isSupportedThinkingTokenModel(model) || isSupportedReasoningEffortModel(model)) &&
|
||||
|
||||
@ -67,7 +67,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 143,
|
||||
version: 144,
|
||||
blacklist: ['runtime', 'messages', 'messageBlocks', 'tabs'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -2321,6 +2321,18 @@ const migrateConfig = {
|
||||
} catch (error) {
|
||||
return state
|
||||
}
|
||||
},
|
||||
'144': (state: RootState) => {
|
||||
try {
|
||||
if (state.settings) {
|
||||
state.settings.confirmDeleteMessage = settingsInitialState.confirmDeleteMessage
|
||||
state.settings.confirmRegenerateMessage = settingsInitialState.confirmRegenerateMessage
|
||||
}
|
||||
return state
|
||||
} catch (error) {
|
||||
logger.error('migrate 144 error', error as Error)
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -122,6 +122,9 @@ export interface SettingsState {
|
||||
enableTopicNaming: boolean
|
||||
customCss: string
|
||||
topicNamingPrompt: string
|
||||
// 消息操作确认设置
|
||||
confirmDeleteMessage: boolean
|
||||
confirmRegenerateMessage: boolean
|
||||
// Sidebar icons
|
||||
sidebarIcons: {
|
||||
visible: SidebarIcon[]
|
||||
@ -346,6 +349,9 @@ export const initialState: SettingsState = {
|
||||
enableSpellCheck: false,
|
||||
spellCheckLanguages: [],
|
||||
enableQuickPanelTriggers: false,
|
||||
// 消息操作确认设置
|
||||
confirmDeleteMessage: true,
|
||||
confirmRegenerateMessage: true,
|
||||
// 硬件加速设置
|
||||
disableHardwareAcceleration: false,
|
||||
exportMenuOptions: {
|
||||
@ -770,6 +776,12 @@ const settingsSlice = createSlice({
|
||||
setEnableQuickPanelTriggers: (state, action: PayloadAction<boolean>) => {
|
||||
state.enableQuickPanelTriggers = action.payload
|
||||
},
|
||||
setConfirmDeleteMessage: (state, action: PayloadAction<boolean>) => {
|
||||
state.confirmDeleteMessage = action.payload
|
||||
},
|
||||
setConfirmRegenerateMessage: (state, action: PayloadAction<boolean>) => {
|
||||
state.confirmRegenerateMessage = action.payload
|
||||
},
|
||||
setDisableHardwareAcceleration: (state, action: PayloadAction<boolean>) => {
|
||||
state.disableHardwareAcceleration = action.payload
|
||||
},
|
||||
@ -949,6 +961,8 @@ export const {
|
||||
setSpellCheckLanguages,
|
||||
setExportMenuOptions,
|
||||
setEnableQuickPanelTriggers,
|
||||
setConfirmDeleteMessage,
|
||||
setConfirmRegenerateMessage,
|
||||
setDisableHardwareAcceleration,
|
||||
setOpenAISummaryText,
|
||||
setOpenAIVerbosity,
|
||||
|
||||
@ -338,6 +338,15 @@ const fetchAndProcessAssistantResponseImpl = async (
|
||||
messagesForContext = contextSlice.filter((m) => m && !m.status?.includes('ing'))
|
||||
}
|
||||
|
||||
// Ensure at least the triggering user message is present to avoid empty payloads
|
||||
if ((!messagesForContext || messagesForContext.length === 0) && userMessageId) {
|
||||
const stateAfter = getState()
|
||||
const maybeUserMessage = stateAfter.messages.entities[userMessageId]
|
||||
if (maybeUserMessage) {
|
||||
messagesForContext = [maybeUserMessage]
|
||||
}
|
||||
}
|
||||
|
||||
callbacks = createCallbacks({
|
||||
blockManager,
|
||||
dispatch,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user