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 && ( + +