From 2480822690dc8f292954565c2bf9e79b1b767486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=A6=8B?= <117180266+oyasumiaiko@users.noreply.github.com> Date: Tue, 2 Sep 2025 11:24:20 +0800 Subject: [PATCH] Remove loading state blocks input, Add "Retry failed messages" button (#9513) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- src/renderer/src/i18n/locales/en-us.json | 5 +- src/renderer/src/i18n/locales/ja-jp.json | 5 +- src/renderer/src/i18n/locales/ru-ru.json | 5 +- src/renderer/src/i18n/locales/zh-cn.json | 5 +- src/renderer/src/i18n/locales/zh-tw.json | 5 +- .../src/pages/home/Inputbar/Inputbar.tsx | 6 +- .../src/pages/home/Messages/Message.tsx | 3 +- .../home/Messages/MessageGroupMenuBar.tsx | 53 +++++++- .../pages/home/Messages/MessageMenubar.tsx | 128 +++++++++++------- .../src/pages/home/Tabs/SettingsTab.tsx | 24 +++- src/renderer/src/services/ApiService.ts | 7 +- src/renderer/src/store/index.ts | 2 +- src/renderer/src/store/migrate.ts | 12 ++ src/renderer/src/store/settings.ts | 14 ++ src/renderer/src/store/thunk/messageThunk.ts | 9 ++ 15 files changed, 221 insertions(+), 62 deletions(-) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index b6093a46bc..c6b745ac85 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -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", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 5745763c80..a00750d71e 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -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": "長いテキストの長さ", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index cb7b2b3c04..d6c6d4d198 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -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": "Длина вставки длинного текста", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 5e5bcc3b42..bf3215a35c 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -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": "长文本长度", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index df75d3da9b..93881b95f9 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -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": "長文字長度", diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index ff637858bd..04f654ad91 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -210,7 +210,7 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = ) const sendMessage = useCallback(async () => { - if (inputEmpty || loading) { + if (inputEmpty) { return } if (checkRateLimit(assistant)) { @@ -258,7 +258,7 @@ const Inputbar: FC = ({ 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 = ({ assistant: _assistant, setActiveTopic, topic }) = onClick={onNewContext} /> + {loading && ( @@ -934,7 +935,6 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = )} - {!loading && } diff --git a/src/renderer/src/pages/home/Messages/Message.tsx b/src/renderer/src/pages/home/Messages/Message.tsx index 10e01bb0b6..48627224ca 100644 --- a/src/renderer/src/pages/home/Messages/Message.tsx +++ b/src/renderer/src/pages/home/Messages/Message.tsx @@ -60,7 +60,6 @@ const MessageItem: FC = ({ index, hideMenuBar = false, isGrouped, - isStreaming = false, onUpdateUseful, isGroupContextMessage }) => { @@ -116,7 +115,7 @@ const MessageItem: FC = ({ 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) => { diff --git a/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx b/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx index 12fcb3f51d..0e7b7ab289 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx @@ -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 = ({ 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 = ({ }) } + 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 = ({ )} {multiModelMessageStyle === 'grid' && } + {hasFailedMessages && ( + +