diff --git a/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch b/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch similarity index 82% rename from .yarn/patches/openai-npm-4.87.3-2b30a7685f.patch rename to .yarn/patches/openai-npm-4.96.0-0665b05cb9.patch index 9970972523..cf4219719c 100644 --- a/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch +++ b/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch @@ -1,8 +1,8 @@ diff --git a/core.js b/core.js -index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb69fe89cb 100644 +index 862d66101f441fb4f47dfc8cff5e2d39e1f5a11e..6464bebbf696c39d35f0368f061ea4236225c162 100644 --- a/core.js +++ b/core.js -@@ -157,7 +157,7 @@ class APIClient { +@@ -159,7 +159,7 @@ class APIClient { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': this.getUserAgent(), @@ -12,10 +12,10 @@ index ebb071d31cd5a14792b62814df072c5971e83300..31e1062d4a7f2422ffec79cf96a35dbb }; } diff --git a/core.mjs b/core.mjs -index 9c1a0264dcd73a85de1cf81df4efab9ce9ee2ab7..33f9f1f237f2eb2667a05dae1a7e3dc916f6bfff 100644 +index 05dbc6cfde51589a2b100d4e4b5b3c1a33b32b89..789fbb4985eb952a0349b779fa83b1a068af6e7e 100644 --- a/core.mjs +++ b/core.mjs -@@ -150,7 +150,7 @@ export class APIClient { +@@ -152,7 +152,7 @@ export class APIClient { Accept: 'application/json', 'Content-Type': 'application/json', 'User-Agent': this.getUserAgent(), diff --git a/docs/technical/Message.md b/docs/technical/Message.md new file mode 100644 index 0000000000..673b1cce7b --- /dev/null +++ b/docs/technical/Message.md @@ -0,0 +1,3 @@ +# 消息的生命周期 + +![image](./message-lifecycle.png) diff --git a/docs/technical/message-lifecycle.png b/docs/technical/message-lifecycle.png new file mode 100644 index 0000000000..95d6c52d1f Binary files /dev/null and b/docs/technical/message-lifecycle.png differ diff --git a/package.json b/package.json index a6ec0c659e..65f569ecdb 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "lucide-react": "^0.487.0", "mime": "^4.0.4", "npx-scope-finder": "^1.2.0", - "openai": "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch", + "openai": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch", "p-queue": "^8.1.0", "prettier": "^3.5.3", "rc-virtual-list": "^3.18.5", @@ -214,10 +214,11 @@ "@langchain/openai@npm:>=0.1.0 <0.4.0": "patch:@langchain/openai@npm%3A0.3.16#~/.yarn/patches/@langchain-openai-npm-0.3.16-e525b59526.patch", "node-gyp": "^9.1.0", "libsql@npm:^0.4.4": "patch:libsql@npm%3A0.4.7#~/.yarn/patches/libsql-npm-0.4.7-444e260fb1.patch", - "openai@npm:^4.77.0": "patch:openai@npm%3A4.87.3#~/.yarn/patches/openai-npm-4.87.3-2b30a7685f.patch", + "openai@npm:^4.77.0": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch", "pkce-challenge@npm:^4.1.0": "patch:pkce-challenge@npm%3A4.1.0#~/.yarn/patches/pkce-challenge-npm-4.1.0-fbc51695a3.patch", "app-builder-lib@npm:26.0.13": "patch:app-builder-lib@npm%3A26.0.13#~/.yarn/patches/app-builder-lib-npm-26.0.13-a064c9e1d0.patch", - "shiki": "3.2.2" + "shiki": "3.2.2", + "openai@npm:^4.87.3": "patch:openai@npm%3A4.96.0#~/.yarn/patches/openai-npm-4.96.0-0665b05cb9.patch" }, "packageManager": "yarn@4.6.0", "lint-staged": { diff --git a/src/renderer/src/components/Spinner.tsx b/src/renderer/src/components/Spinner.tsx new file mode 100644 index 0000000000..fb8e3d35e7 --- /dev/null +++ b/src/renderer/src/components/Spinner.tsx @@ -0,0 +1,41 @@ +import { Search } from 'lucide-react' +import { useTranslation } from 'react-i18next' +import BarLoader from 'react-spinners/BarLoader' +import styled, { css } from 'styled-components' + +interface Props { + text: string +} + +export default function Spinner({ text }: Props) { + const { t } = useTranslation() + return ( + + + {t(text)} + + + ) +} + +const baseContainer = css` + display: flex; + flex-direction: row; + align-items: center; +` + +const Container = styled.div` + ${baseContainer} + background-color: var(--color-background-mute); + padding: 10px; + border-radius: 10px; + margin-bottom: 10px; + gap: 10px; +` + +const StatusText = styled.div` + font-size: 14px; + line-height: 1.6; + text-decoration: none; + color: var(--color-text-1); +` diff --git a/src/renderer/src/components/TranslateButton.tsx b/src/renderer/src/components/TranslateButton.tsx index 8ebdef2767..010e152482 100644 --- a/src/renderer/src/components/TranslateButton.tsx +++ b/src/renderer/src/components/TranslateButton.tsx @@ -2,8 +2,7 @@ import { LoadingOutlined } from '@ant-design/icons' import { useDefaultModel } from '@renderer/hooks/useAssistant' import { useSettings } from '@renderer/hooks/useSettings' import { fetchTranslate } from '@renderer/services/ApiService' -import { getDefaultTopic, getDefaultTranslateAssistant } from '@renderer/services/AssistantService' -import { getUserMessage } from '@renderer/services/MessagesService' +import { getDefaultTranslateAssistant } from '@renderer/services/AssistantService' import { Button, Tooltip } from 'antd' import { Languages } from 'lucide-react' import { FC, useEffect, useState } from 'react' @@ -36,6 +35,7 @@ const TranslateButton: FC = ({ text, onTranslated, disabled, style, isLoa } const handleTranslate = async () => { + console.log('handleTranslate', text) if (!text?.trim()) return if (!(await translateConfirm())) { @@ -56,14 +56,7 @@ const TranslateButton: FC = ({ text, onTranslated, disabled, style, isLoa setIsTranslating(true) try { const assistant = getDefaultTranslateAssistant(targetLanguage, text) - const message = getUserMessage({ - assistant, - topic: getDefaultTopic('default'), - type: 'text', - content: '' - }) - - const translatedText = await fetchTranslate({ message, assistant }) + const translatedText = await fetchTranslate({ content: text, assistant }) onTranslated(translatedText) } catch (error) { console.error('Translation failed:', error) diff --git a/src/renderer/src/databases/index.ts b/src/renderer/src/databases/index.ts index c942e678a0..b75c3497a9 100644 --- a/src/renderer/src/databases/index.ts +++ b/src/renderer/src/databases/index.ts @@ -1,16 +1,19 @@ -import { FileType, KnowledgeItem, QuickPhrase, Topic, TranslateHistory } from '@renderer/types' +import { FileType, KnowledgeItem, QuickPhrase, TranslateHistory } from '@renderer/types' +// Import necessary types for blocks and new message structure +import type { Message as NewMessage, MessageBlock } from '@renderer/types/newMessage' import { Dexie, type EntityTable } from 'dexie' -import { upgradeToV5 } from './upgrades' +import { upgradeToV5, upgradeToV7 } from './upgrades' // Database declaration (move this to its own module also) export const db = new Dexie('CherryStudio') as Dexie & { files: EntityTable - topics: EntityTable, 'id'> + topics: EntityTable<{ id: string; messages: NewMessage[] }, 'id'> // Correct type for topics settings: EntityTable<{ id: string; value: any }, 'id'> knowledge_notes: EntityTable translate_history: EntityTable quick_phrases: EntityTable + message_blocks: EntityTable // Correct type for message_blocks } db.version(1).stores({ @@ -57,4 +60,18 @@ db.version(6).stores({ quick_phrases: 'id' }) +// --- NEW VERSION 7 --- +db.version(7) + .stores({ + // Re-declare all tables for the new version + files: 'id, name, origin_name, path, size, ext, type, created_at, count', + topics: '&id', // Correct index for topics + settings: '&id, value', + knowledge_notes: '&id, baseId, type, content, created_at, updated_at', + translate_history: '&id, sourceText, targetText, sourceLanguage, targetLanguage, createdAt', + quick_phrases: 'id', + message_blocks: 'id, messageId, file.id' // Correct syntax with comma separator + }) + .upgrade((tx) => upgradeToV7(tx)) + export default db diff --git a/src/renderer/src/databases/upgrades.ts b/src/renderer/src/databases/upgrades.ts index f4a07f91ec..3303ae467a 100644 --- a/src/renderer/src/databases/upgrades.ts +++ b/src/renderer/src/databases/upgrades.ts @@ -1,5 +1,26 @@ +import type { LegacyMessage as OldMessage, Topic } from '@renderer/types' +import { FileTypes } from '@renderer/types' // Import FileTypes enum +import { WebSearchSource } from '@renderer/types' +import type { + BaseMessageBlock, + CitationMessageBlock, + Message as NewMessage, + MessageBlock +} from '@renderer/types/newMessage' +import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage' import { Transaction } from 'dexie' +import { + createCitationBlock, + createErrorBlock, + createFileBlock, + createImageBlock, + createMainTextBlock, + createThinkingBlock, + createToolBlock, + createTranslationBlock +} from '../utils/messageUtils/create' + export async function upgradeToV5(tx: Transaction): Promise { const topics = await tx.table('topics').toArray() const files = await tx.table('files').toArray() @@ -37,18 +58,247 @@ export async function upgradeToV5(tx: Transaction): Promise { } } -// 为每个 topic 添加时间戳,兼容老数据,默认按照最新的时间戳来,不确定是否要加 -export async function upgradeToV6(tx: Transaction): Promise { - const topics = await tx.table('topics').toArray() - - // 为每个 topic 添加时间戳,兼容老数据,默认按照最新的时间戳来 - const now = new Date().toISOString() - for (const topic of topics) { - if (!topic.createdAt && !topic.updatedAt) { - await tx.table('topics').update(topic.id, { - createdAt: now, - updatedAt: now - }) - } +// --- Simplified status mapping functions --- +function mapOldStatusToBlockStatus(oldStatus: OldMessage['status']): MessageBlockStatus { + // Handle statuses that need mapping + if (oldStatus === 'sending' || oldStatus === 'pending' || oldStatus === 'searching') { + return MessageBlockStatus.PROCESSING } + // For success, paused, error, the values match MessageBlockStatus + if (oldStatus === 'success' || oldStatus === 'paused' || oldStatus === 'error') { + // Cast is safe here as the values are identical + return oldStatus as MessageBlockStatus + } + // Default fallback for any unexpected old status + return MessageBlockStatus.PROCESSING +} + +function mapOldStatusToNewMessageStatus(oldStatus: OldMessage['status']): NewMessage['status'] { + // Handle statuses that need mapping + if (oldStatus === 'pending' || oldStatus === 'sending') { + return AssistantMessageStatus.PENDING + } + // For sending, success, paused, error, the values match NewMessage['status'] + if (oldStatus === 'searching' || oldStatus === 'success' || oldStatus === 'paused' || oldStatus === 'error') { + // Cast is safe here as the values are identical + return oldStatus as NewMessage['status'] + } + // Default fallback + return AssistantMessageStatus.PROCESSING +} + +// --- UPDATED UPGRADE FUNCTION for Version 7 --- +export async function upgradeToV7(tx: Transaction): Promise { + console.log('Starting DB migration to version 7: Normalizing messages and blocks...') + + const oldTopicsTable = tx.table('topics') + const newBlocksTable = tx.table('message_blocks') + const topicUpdates: Record = {} + + await oldTopicsTable.toCollection().each(async (oldTopic: Pick & { messages: OldMessage[] }) => { + const newMessagesForTopic: NewMessage[] = [] + const blocksToCreate: MessageBlock[] = [] + + if (!oldTopic.messages || !Array.isArray(oldTopic.messages)) { + console.warn(`Topic ${oldTopic.id} has no valid messages array, skipping.`) + topicUpdates[oldTopic.id] = { messages: [] } + return + } + + for (const oldMessage of oldTopic.messages) { + const messageBlockIds: string[] = [] + const citationDataToCreate: Partial> = {} + let hasCitationData = false + + // 1. Main Text Block + if (oldMessage.content?.trim()) { + const block = createMainTextBlock(oldMessage.id, oldMessage.content, { + createdAt: oldMessage.createdAt, + status: mapOldStatusToBlockStatus(oldMessage.status), + knowledgeBaseIds: oldMessage.knowledgeBaseIds + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } + + // 2. Thinking Block (Status is SUCCESS) + if (oldMessage.reasoning_content?.trim()) { + const block = createThinkingBlock(oldMessage.id, oldMessage.reasoning_content, { + createdAt: oldMessage.createdAt, + status: MessageBlockStatus.SUCCESS // Thinking block is complete content + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } + + // 3. Translation Block (Status is SUCCESS) + if (oldMessage.translatedContent?.trim()) { + const block = createTranslationBlock(oldMessage.id, oldMessage.translatedContent, 'unknown', { + createdAt: oldMessage.createdAt, + status: MessageBlockStatus.SUCCESS // Translation block is complete content + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } + + // 4. File Blocks (Non-Image) and Image Blocks (from Files) (Status is SUCCESS) + if (oldMessage.files?.length) { + oldMessage.files.forEach((file) => { + if (file.type === FileTypes.IMAGE) { + const block = createImageBlock(oldMessage.id, { + file: file, + createdAt: oldMessage.createdAt, + status: MessageBlockStatus.SUCCESS + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } else { + const block = createFileBlock(oldMessage.id, file, { + createdAt: oldMessage.createdAt, + status: MessageBlockStatus.SUCCESS + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } + }) + } + + // 5. Image Blocks (from Metadata - AI Generated) (Status is SUCCESS) + if (oldMessage.metadata?.generateImage) { + const block = createImageBlock(oldMessage.id, { + metadata: { generateImageResponse: oldMessage.metadata.generateImage }, + createdAt: oldMessage.createdAt, + status: MessageBlockStatus.SUCCESS + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } + + // 6. Web Search Block - REMOVED, data moved to citation collection + // if (oldMessage.metadata?.webSearch?.results?.length) { ... } + + // 7. Tool Blocks (Status based on original mcpTool status) + if (oldMessage.metadata?.mcpTools?.length) { + oldMessage.metadata.mcpTools.forEach((mcpTool) => { + const block = createToolBlock(oldMessage.id, mcpTool.id, { + // Determine status based on original tool status + status: MessageBlockStatus.SUCCESS, + content: mcpTool.response, + error: + mcpTool.status !== 'done' + ? { message: 'MCP Tool did not complete', originalStatus: mcpTool.status } + : undefined, + createdAt: oldMessage.createdAt, + metadata: { rawMcpToolResponse: mcpTool } + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + }) + } + + // 8. Collect Citation and Reference Data (Simplified: Independent checks) + if (oldMessage.metadata?.groundingMetadata) { + hasCitationData = true + citationDataToCreate.response = { + results: oldMessage.metadata.groundingMetadata, + source: WebSearchSource.GEMINI + } + } + if (oldMessage.metadata?.annotations?.length) { + hasCitationData = true + citationDataToCreate.response = { + results: oldMessage.metadata.annotations, + source: WebSearchSource.OPENAI + } + } + if (oldMessage.metadata?.citations?.length) { + hasCitationData = true + citationDataToCreate.response = { + results: oldMessage.metadata.citations, + // 无法区分,统一为Openrouter + source: WebSearchSource.OPENROUTER + } + } + if (oldMessage.metadata?.webSearch) { + hasCitationData = true + citationDataToCreate.response = { + results: oldMessage.metadata.webSearch?.results, + source: WebSearchSource.WEBSEARCH + } + } + if (oldMessage.metadata?.webSearchInfo) { + hasCitationData = true + citationDataToCreate.response = { + results: oldMessage.metadata.webSearchInfo, + // 无法区分,统一为zhipu + source: WebSearchSource.ZHIPU + } + } + if (oldMessage.metadata?.knowledge?.length) { + hasCitationData = true + citationDataToCreate.knowledge = oldMessage.metadata.knowledge + } + + // 9. Create Citation Block (if any citation data was found, no need to set citationType) + if (hasCitationData) { + const block = createCitationBlock( + oldMessage.id, + citationDataToCreate as Omit, + { + createdAt: oldMessage.createdAt, + status: MessageBlockStatus.SUCCESS + } + ) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } + + // 10. Error Block (Status is ERROR) + if (oldMessage.error && typeof oldMessage.error === 'object' && Object.keys(oldMessage.error).length > 0) { + const block = createErrorBlock(oldMessage.id, oldMessage.error, { + createdAt: oldMessage.createdAt, + status: MessageBlockStatus.ERROR // Error block status is ERROR + }) + blocksToCreate.push(block) + messageBlockIds.push(block.id) + } + + // 11. Create the New Message reference object (Add usage/metrics assignment) + const newMessageReference: NewMessage = { + id: oldMessage.id, + role: oldMessage.role as NewMessage['role'], + assistantId: oldMessage.assistantId || '', + topicId: oldTopic.id, + createdAt: oldMessage.createdAt, + status: mapOldStatusToNewMessageStatus(oldMessage.status), + modelId: oldMessage.modelId, + model: oldMessage.model, + type: oldMessage.type === 'clear' ? 'clear' : undefined, + isPreset: oldMessage.isPreset, + useful: oldMessage.useful, + askId: oldMessage.askId, + mentions: oldMessage.mentions, + enabledMCPs: oldMessage.enabledMCPs, + usage: oldMessage.usage, + metrics: oldMessage.metrics, + multiModelMessageStyle: oldMessage.multiModelMessageStyle, + foldSelected: oldMessage.foldSelected, + blocks: messageBlockIds + } + newMessagesForTopic.push(newMessageReference) + } + + if (blocksToCreate.length > 0) { + await newBlocksTable.bulkPut(blocksToCreate) + } + topicUpdates[oldTopic.id] = { messages: newMessagesForTopic } + }) + + const updateOperations = Object.entries(topicUpdates).map(([id, data]) => ({ key: id, changes: data })) + if (updateOperations.length > 0) { + await oldTopicsTable.bulkUpdate(updateOperations) + console.log(`Updated message references for ${updateOperations.length} topics.`) + } + + console.log('DB migration to version 7 finished successfully.') } diff --git a/src/renderer/src/hooks/useMessageOperations.ts b/src/renderer/src/hooks/useMessageOperations.ts index 05d072c7e2..0511127771 100644 --- a/src/renderer/src/hooks/useMessageOperations.ts +++ b/src/renderer/src/hooks/useMessageOperations.ts @@ -1,231 +1,306 @@ +import { createSelector } from '@reduxjs/toolkit' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' -import { estimateMessageUsage } from '@renderer/services/TokenService' -import store, { useAppDispatch, useAppSelector } from '@renderer/store' +import store, { type RootState, useAppDispatch, useAppSelector } from '@renderer/store' +import { messageBlocksSelectors } from '@renderer/store/messageBlock' +import { updateOneBlock } from '@renderer/store/messageBlock' +import { newMessagesActions, selectMessagesForTopic } from '@renderer/store/newMessage' import { - clearStreamMessage, - clearTopicMessages, - commitStreamMessage, - deleteMessageAction, - resendMessage, - selectDisplayCount, - selectTopicLoading, - selectTopicMessages, - setStreamMessage, - setTopicLoading, - updateMessages, - updateMessageThunk -} from '@renderer/store/messages' -import type { Assistant, Message, Topic } from '@renderer/types' + appendAssistantResponseThunk, + clearTopicMessagesThunk, + cloneMessagesToNewTopicThunk, + deleteMessageGroupThunk, + deleteSingleMessageThunk, + initiateTranslationThunk, + regenerateAssistantResponseThunk, + resendMessageThunk, + resendUserMessageWithEditThunk +} from '@renderer/store/thunk/messageThunk' +import { throttledBlockDbUpdate } from '@renderer/store/thunk/messageThunk' +import type { Assistant, Model, Topic } from '@renderer/types' +import type { Message, MessageBlock } from '@renderer/types/newMessage' +import { MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage' import { abortCompletion } from '@renderer/utils/abortController' import { useCallback } from 'react' -import { TopicManager } from './useTopic' +const findMainTextBlockId = (message: Message): string | undefined => { + if (!message || !message.blocks) return undefined + const state = store.getState() + for (const blockId of message.blocks) { + const block = messageBlocksSelectors.selectById(state, String(blockId)) + if (block && block.type === MessageBlockType.MAIN_TEXT) { + return block.id + } + } + return undefined +} + +const selectMessagesState = (state: RootState) => state.messages + +export const selectNewTopicLoading = createSelector( + [selectMessagesState, (_, topicId: string) => topicId], + (messagesState, topicId) => messagesState.loadingByTopic[topicId] || false +) + +export const selectNewDisplayCount = createSelector( + [selectMessagesState], + (messagesState) => messagesState.displayCount +) + /** - * 自定义Hook,提供消息操作相关的功能 - * - * @param topic 当前主题 - * @returns 一组消息操作方法 + * Hook 提供针对特定主题的消息操作方法。 / Hook providing various operations for messages within a specific topic. + * @param topic 当前主题对象。 / The current topic object. + * @returns 包含消息操作函数的对象。 / An object containing message operation functions. */ export function useMessageOperations(topic: Topic) { const dispatch = useAppDispatch() /** - * 删除单个消息 + * 删除单个消息。 / Deletes a single message. + * Dispatches deleteSingleMessageThunk. */ const deleteMessage = useCallback( async (id: string) => { - await dispatch(deleteMessageAction(topic, id)) + await dispatch(deleteSingleMessageThunk(topic.id, id)) }, - [dispatch, topic] + [dispatch, topic.id] // Use topic.id directly ) /** - * 删除一组消息(基于askId) + * 删除一组消息(基于 askId)。 / Deletes a group of messages (based on askId). + * Dispatches deleteMessageGroupThunk. */ const deleteGroupMessages = useCallback( async (askId: string) => { - await dispatch(deleteMessageAction(topic, askId, 'askId')) + await dispatch(deleteMessageGroupThunk(topic.id, askId)) }, - [dispatch, topic] + [dispatch, topic.id] ) /** - * 编辑消息内容 + * 编辑消息。(目前仅更新 Redux state)。 / Edits a message. (Currently only updates Redux state). + * 使用 newMessagesActions.updateMessage. */ const editMessage = useCallback( async (messageId: string, updates: Partial) => { - // 如果更新包含内容变更,重新计算 token - if ('content' in updates) { - const messages = store.getState().messages.messagesByTopic[topic.id] - const message = messages?.find((m) => m.id === messageId) - if (message) { - const updatedMessage = { ...message, ...updates } - const usage = await estimateMessageUsage(updatedMessage) - updates.usage = usage - } - } - await dispatch(updateMessageThunk(topic.id, messageId, updates)) + // Basic update remains the same + await dispatch(newMessagesActions.updateMessage({ topicId: topic.id, messageId, updates })) + // TODO: Add token recalculation logic here if necessary + // if ('content' in updates or other relevant fields change) { + // const state = store.getState(); // Need store or selector access + // const message = state.messages.messagesByTopic[topic.id]?.find(m => m.id === messageId); + // if (message) { + // const updatedUsage = await estimateTokenUsage(...); // Call estimation service + // await dispatch(newMessagesActions.updateMessage({ topicId: topic.id, messageId, updates: { usage: updatedUsage } })); + // } + // } }, [dispatch, topic.id] ) /** - * 重新发送消息 + * 重新发送用户消息,触发其所有助手回复的重新生成。 / Resends a user message, triggering regeneration of all its assistant responses. + * Dispatches resendMessageThunk. */ - const resendMessageAction = useCallback( - async (message: Message, assistant: Assistant, isMentionModel = false) => { - return dispatch(resendMessage(message, assistant, topic, isMentionModel)) + const resendMessage = useCallback( + async (message: Message, assistant: Assistant) => { + await dispatch(resendMessageThunk(topic.id, message, assistant)) }, - [dispatch, topic] + [dispatch, topic.id] // topic object needed by thunk ) /** - * 重新发送用户消息(编辑后) + * 在用户消息的主文本块被编辑后重新发送该消息。 / Resends a user message after its main text block has been edited. + * Dispatches resendUserMessageWithEditThunk. */ const resendUserMessageWithEdit = useCallback( async (message: Message, editedContent: string, assistant: Assistant) => { - // 先更新消息内容 - await editMessage(message.id, { content: editedContent }) - // 然后重新发送 - return dispatch(resendMessage({ ...message, content: editedContent }, assistant, topic)) + const mainTextBlockId = findMainTextBlockId(message) + if (!mainTextBlockId) { + console.error('Cannot resend edited message: Main text block not found.') + return + } + + await dispatch(resendUserMessageWithEditThunk(topic.id, message, mainTextBlockId, editedContent, assistant)) }, - [dispatch, editMessage, topic] + [dispatch, topic.id] // topic object needed by thunk ) /** - * 设置流式消息 + * 清除当前或指定主题的所有消息。 / Clears all messages for the current or specified topic. + * Dispatches clearTopicMessagesThunk. */ - const setStreamMessageAction = useCallback( - (message: Message | null) => { - dispatch(setStreamMessage({ topicId: topic.id, message })) - }, - [dispatch, topic.id] - ) - - /** - * 提交流式消息 - */ - const commitStreamMessageAction = useCallback( - (messageId: string) => { - dispatch(commitStreamMessage({ topicId: topic.id, messageId })) - }, - [dispatch, topic.id] - ) - - /** - * 清除流式消息 - */ - const clearStreamMessageAction = useCallback( - (messageId: string) => { - dispatch(clearStreamMessage({ topicId: topic.id, messageId })) - }, - [dispatch, topic.id] - ) - - /** - * 清除会话消息 - */ - const clearTopicMessagesAction = useCallback( + const clearTopicMessages = useCallback( async (_topicId?: string) => { - const topicId = _topicId || topic.id - await dispatch(clearTopicMessages(topicId)) - await TopicManager.clearTopicMessages(topicId) + const topicIdToClear = _topicId || topic.id + await dispatch(clearTopicMessagesThunk(topicIdToClear)) }, [dispatch, topic.id] ) /** - * 更新消息数据 - */ - const updateMessagesAction = useCallback( - async (messages: Message[]) => { - await dispatch(updateMessages(topic, messages)) - }, - [dispatch, topic] - ) - - /** - * 创建新的上下文(clear message) + * 发出事件以表示创建新上下文(清空消息 UI)。 / Emits an event to signal creating a new context (clearing messages UI). */ const createNewContext = useCallback(async () => { EventEmitter.emit(EVENT_NAMES.NEW_CONTEXT) }, []) - const displayCount = useAppSelector(selectDisplayCount) - // /** - // * 获取当前消息列表 - // */ - // const getMessages = useCallback(() => messages, [messages]) + const displayCount = useAppSelector(selectNewDisplayCount) /** - * 暂停消息生成 + * 暂停当前主题正在进行的消息生成。 / Pauses ongoing message generation for the current topic. */ - // const pauseMessage = useCallback( - // // 存的是用户消息的id,也就是助手消息的askId - // async (message: Message) => { - // // 1. 调用 abort - - // // 2. 更新消息状态, - // // await editMessage(message.id, { status: 'paused', content: message.content }) - - // // 3.更改loading状态 - // dispatch(setTopicLoading({ topicId: message.topicId, loading: false })) - - // // 4. 清理流式消息 - // // clearStreamMessageAction(message.id) - // }, - // [editMessage, dispatch, clearStreamMessageAction] - // ) - const pauseMessages = useCallback(async () => { - // 暂停的消息不需要在这更改status,通过catch判断abort错误之后设置message.status - const streamMessages = store.getState().messages.streamMessagesByTopic[topic.id] - if (!streamMessages) return - // 不需要重复暂停 - const askIds = [...new Set(Object.values(streamMessages).map((m) => m?.askId))] + // Use selector if preferred, but direct access is okay in callback + const state = store.getState() + const topicMessages = selectMessagesForTopic(state, topic.id) + if (!topicMessages) return + + // Find messages currently in progress (adjust statuses if needed) + const streamingMessages = topicMessages.filter((m) => m.status === 'processing' || m.status === 'pending') + + const askIds = [...new Set(streamingMessages?.map((m) => m.askId).filter((id) => !!id) as string[])] for (const askId of askIds) { - askId && abortCompletion(askId) + abortCompletion(askId) } - dispatch(setTopicLoading({ topicId: topic.id, loading: false })) + // Ensure loading state is set to false + dispatch(newMessagesActions.setTopicLoading({ topicId: topic.id, loading: false })) }, [topic.id, dispatch]) /** - * 恢复/重发消息 - * 暂时不需要 + * 恢复/重发用户消息(目前复用 resendMessage 逻辑)。 / Resumes/Resends a user message (currently reuses resendMessage logic). */ const resumeMessage = useCallback( async (message: Message, assistant: Assistant) => { - return resendMessageAction(message, assistant) + // Directly call the resendMessage function from this hook + return resendMessage(message, assistant) }, - [resendMessageAction] + [resendMessage] // Dependency is the resendMessage function itself + ) + + /** + * 重新生成指定的助手消息回复。 / Regenerates a specific assistant message response. + * Dispatches regenerateAssistantResponseThunk. + */ + const regenerateAssistantMessage = useCallback( + async (message: Message, assistant: Assistant) => { + if (message.role !== 'assistant') { + console.warn('regenerateAssistantMessage should only be called for assistant messages.') + return + } + await dispatch(regenerateAssistantResponseThunk(topic.id, message, assistant)) + }, + [dispatch, topic.id] // topic object needed by thunk + ) + + /** + * 使用指定模型追加一个新的助手回复,回复与现有助手消息相同的用户查询。 / Appends a new assistant response using a specified model, replying to the same user query as an existing assistant message. + * Dispatches appendAssistantResponseThunk. + */ + const appendAssistantResponse = useCallback( + async (existingAssistantMessage: Message, newModel: Model, assistant: Assistant) => { + if (existingAssistantMessage.role !== 'assistant') { + console.error('appendAssistantResponse should only be called for an existing assistant message.') + return + } + if (!existingAssistantMessage.askId) { + console.error('Cannot append response: The existing assistant message is missing its askId.') + return + } + await dispatch(appendAssistantResponseThunk(topic.id, existingAssistantMessage.id, newModel, assistant)) + }, + [dispatch, topic.id] // Dependencies + ) + + /** + * 初始化翻译块并返回一个更新函数。 / Initiates a translation block and returns an updater function. + * @param messageId 要翻译的消息 ID。 / The ID of the message to translate. + * @param targetLanguage 目标语言代码。 / The target language code. + * @param sourceBlockId (可选) 源块的 ID。 / (Optional) The ID of the source block. + * @param sourceLanguage (可选) 源语言代码。 / (Optional) The source language code. + * @returns 用于更新翻译块的异步函数,如果初始化失败则返回 null。 / An async function to update the translation block, or null if initiation fails. + */ + const getTranslationUpdater = useCallback( + async ( + messageId: string, + targetLanguage: string, + sourceBlockId?: string, + sourceLanguage?: string + ): Promise<((accumulatedText: string, isComplete?: boolean) => void) | null> => { + if (!topic.id) return null + + // 1. Initiate the block and get its ID + const blockId = await dispatch( + initiateTranslationThunk(messageId, topic.id, targetLanguage, sourceBlockId, sourceLanguage) + ) + + if (!blockId) { + console.error('[getTranslationUpdater] Failed to initiate translation block.') + return null + } + + // 2. Return the updater function + // TODO:下面这个逻辑也可以放在thunk中 + return (accumulatedText: string, isComplete: boolean = false) => { + const status = isComplete ? MessageBlockStatus.SUCCESS : MessageBlockStatus.STREAMING + const changes: Partial = { content: accumulatedText, status: status } // Use Partial + + // Dispatch update to Redux store + dispatch(updateOneBlock({ id: blockId, changes })) + + // Throttle update to DB + throttledBlockDbUpdate(blockId, changes) // Use the throttled function + + // if (isComplete) { + // console.log(`[TranslationUpdater] Final update for block ${blockId}.`) + // // Ensure the throttled function flushes if needed, or call an immediate save + // // For simplicity, we rely on the throttle's trailing call for now. + // } + } + }, + [dispatch, topic.id] + ) + + /** + * 创建一个主题分支,克隆消息到新主题。 + * Creates a topic branch by cloning messages to a new topic. + * @param sourceTopicId 源主题ID / Source topic ID + * @param branchPointIndex 分支点索引,此索引之前的消息将被克隆 / Branch point index, messages before this index will be cloned + * @param newTopic 新的主题对象,必须已经创建并添加到Redux store中 / New topic object, must be already created and added to Redux store + * @returns 操作是否成功 / Whether the operation was successful + */ + const createTopicBranch = useCallback( + (sourceTopicId: string, branchPointIndex: number, newTopic: Topic) => { + console.log(`Cloning messages from topic ${sourceTopicId} to new topic ${newTopic.id}`) + return dispatch(cloneMessagesToNewTopicThunk(sourceTopicId, branchPointIndex, newTopic)) + }, + [dispatch] ) return { displayCount, - updateMessages: updateMessagesAction, deleteMessage, deleteGroupMessages, editMessage, - resendMessage: resendMessageAction, + resendMessage, + regenerateAssistantMessage, resendUserMessageWithEdit, - setStreamMessage: setStreamMessageAction, - commitStreamMessage: commitStreamMessageAction, - clearStreamMessage: clearStreamMessageAction, + appendAssistantResponse, createNewContext, - clearTopicMessages: clearTopicMessagesAction, - // pauseMessage, + clearTopicMessages, pauseMessages, - resumeMessage + resumeMessage, + getTranslationUpdater, + createTopicBranch } } export const useTopicMessages = (topic: Topic) => { - const messages = useAppSelector((state) => selectTopicMessages(state, topic.id)) + const messages = useAppSelector((state) => selectMessagesForTopic(state, topic.id)) return messages } export const useTopicLoading = (topic: Topic) => { - const loading = useAppSelector((state) => selectTopicLoading(state, topic.id)) + const loading = useAppSelector((state) => selectNewTopicLoading(state, topic.id)) return loading } diff --git a/src/renderer/src/hooks/useTopic.ts b/src/renderer/src/hooks/useTopic.ts index cf5c1e7310..2e6d4eb724 100644 --- a/src/renderer/src/hooks/useTopic.ts +++ b/src/renderer/src/hooks/useTopic.ts @@ -3,8 +3,9 @@ import i18n from '@renderer/i18n' import { deleteMessageFiles } from '@renderer/services/MessagesService' import store from '@renderer/store' import { updateTopic } from '@renderer/store/assistants' -import { prepareTopicMessages } from '@renderer/store/messages' +import { loadTopicMessagesThunk } from '@renderer/store/thunk/messageThunk' import { Assistant, Topic } from '@renderer/types' +import { findMainTextBlocks } from '@renderer/utils/messageUtils/find' import { find, isEmpty } from 'lodash' import { useEffect, useState } from 'react' @@ -25,7 +26,7 @@ export function useActiveTopic(_assistant: Assistant, topic?: Topic) { useEffect(() => { if (activeTopic) { - store.dispatch(prepareTopicMessages(activeTopic)) + store.dispatch(loadTopicMessagesThunk(activeTopic.id)) } }, [activeTopic]) @@ -75,7 +76,12 @@ export const autoRenameTopic = async (assistant: Assistant, topicId: string) => } if (!enableTopicNaming) { - const topicName = topic.messages[0]?.content.substring(0, 50) + const message = topic.messages[0] + const blocks = findMainTextBlocks(message) + const topicName = blocks + .map((block) => block.content) + .join('\n\n') + .substring(0, 50) if (topicName) { const data = { ...topic, name: topicName } as Topic _setActiveTopic(data) diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 7307618bfc..82963d1642 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -545,6 +545,7 @@ "message.style": "Message style", "message.style.bubble": "Bubble", "message.style.plain": "Plain", + "processing": "Processing...", "regenerate.confirm": "Regenerating will replace current message", "reset.confirm.content": "Are you sure you want to clear all data?", "reset.double.confirm.content": "All data will be lost, do you want to continue?", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 510fd0d257..7d4b5b28af 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -544,6 +544,7 @@ "message.style": "メッセージスタイル", "message.style.bubble": "バブル", "message.style.plain": "プレーン", + "processing": "処理中...", "regenerate.confirm": "再生成すると現在のメッセージが置き換えられます", "reset.confirm.content": "すべてのデータをリセットしてもよろしいですか?", "reset.double.confirm.content": "すべてのデータが失われます。続行しますか?", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index f0919d7662..210a76d559 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -545,6 +545,7 @@ "message.style": "Стиль сообщения", "message.style.bubble": "Пузырь", "message.style.plain": "Простой", + "processing": "Обрабатывается...", "regenerate.confirm": "Перегенерация заменит текущее сообщение", "reset.confirm.content": "Вы уверены, что хотите очистить все данные?", "reset.double.confirm.content": "Все данные будут утеряны, хотите продолжить?", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c15510cf26..cc092161f7 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -545,6 +545,7 @@ "message.style": "消息样式", "message.style.bubble": "气泡", "message.style.plain": "简洁", + "processing": "正在处理...", "regenerate.confirm": "重新生成会覆盖当前消息", "reset.confirm.content": "确定要重置所有数据吗?", "reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index dfab7788ce..3afbe14b59 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -545,6 +545,7 @@ "message.style": "訊息樣式", "message.style.bubble": "氣泡", "message.style.plain": "簡潔", + "processing": "正在處理...", "regenerate.confirm": "重新生成會覆蓋目前訊息", "reset.confirm.content": "確定要清除所有資料嗎?", "reset.double.confirm.content": "所有資料將會被清除,您確定要繼續嗎?", diff --git a/src/renderer/src/pages/files/FilesPage.tsx b/src/renderer/src/pages/files/FilesPage.tsx index e78f693ba1..d1e89ed6b0 100644 --- a/src/renderer/src/pages/files/FilesPage.tsx +++ b/src/renderer/src/pages/files/FilesPage.tsx @@ -12,6 +12,7 @@ import db from '@renderer/databases' import FileManager from '@renderer/services/FileManager' import store from '@renderer/store' import { FileType, FileTypes } from '@renderer/types' +import { Message } from '@renderer/types/newMessage' import { formatFileSize } from '@renderer/utils' import { Button, Empty, Flex, Popconfirm } from 'antd' import dayjs from 'dayjs' @@ -71,6 +72,7 @@ const FilesPage: FC = () => { const handleDelete = async (fileId: string) => { const file = await FileManager.getFile(fileId) + if (!file) return const paintings = await store.getState().paintings.paintings const paintingsFiles = paintings.flatMap((p) => p.files) @@ -79,23 +81,81 @@ const FilesPage: FC = () => { window.modal.warning({ content: t('files.delete.paintings.warning'), centered: true }) return } - if (file) { await FileManager.deleteFile(fileId, true) } - const topics = await db.topics - .filter((topic) => topic.messages.some((message) => message.files?.some((f) => f.id === fileId))) - .toArray() + const relatedBlocks = await db.message_blocks.where('file.id').equals(fileId).toArray() - if (topics.length > 0) { - for (const topic of topics) { - const updatedMessages = topic.messages.map((message) => ({ - ...message, - files: message.files?.filter((f) => f.id !== fileId) - })) - await db.topics.update(topic.id, { messages: updatedMessages }) + const blockIdsToDelete = relatedBlocks.map((block) => block.id) + + const blocksByMessageId: Record = {} + for (const block of relatedBlocks) { + if (!blocksByMessageId[block.messageId]) { + blocksByMessageId[block.messageId] = [] } + blocksByMessageId[block.messageId].push(block.id) + } + + try { + const affectedMessageIds = [...new Set(relatedBlocks.map((b) => b.messageId))] + + if (affectedMessageIds.length === 0 && blockIdsToDelete.length > 0) { + // This case should ideally not happen if relatedBlocks were found, + // but handle it just in case: only delete blocks. + await db.message_blocks.bulkDelete(blockIdsToDelete) + console.log( + `Deleted ${blockIdsToDelete.length} blocks related to file ${fileId}. No associated messages found (unexpected).` + ) + return + } + + await db.transaction('rw', db.topics, db.message_blocks, async () => { + // Fetch all topics (potential performance bottleneck if many topics) + const allTopics = await db.topics.toArray() + const topicsToUpdate: Record = {} // Store updates keyed by topicId + + for (const topic of allTopics) { + let topicModified = false + // Ensure topic.messages exists and is an array before mapping + const currentMessages = Array.isArray(topic.messages) ? topic.messages : [] + const updatedMessages = currentMessages.map((message) => { + // Check if this message is affected + if (affectedMessageIds.includes(message.id)) { + // Ensure message.blocks exists and is an array + const currentBlocks = Array.isArray(message.blocks) ? message.blocks : [] + const originalBlockCount = currentBlocks.length + // Filter out the blocks marked for deletion + const newBlocks = currentBlocks.filter((blockId) => !blockIdsToDelete.includes(blockId)) + if (newBlocks.length < originalBlockCount) { + topicModified = true + return { ...message, blocks: newBlocks } // Return updated message + } + } + return message // Return original message + }) + + if (topicModified) { + // Store the update for this topic + topicsToUpdate[topic.id] = { messages: updatedMessages } + } + } + + // Apply updates to topics + const updatePromises = Object.entries(topicsToUpdate).map(([topicId, updateData]) => + db.topics.update(topicId, updateData) + ) + await Promise.all(updatePromises) + + // Finally, delete the MessageBlocks + await db.message_blocks.bulkDelete(blockIdsToDelete) + }) + + console.log(`Deleted ${blockIdsToDelete.length} blocks and updated relevant topic messages for file ${fileId}.`) + } catch (error) { + console.error(`Error updating topics or deleting blocks for file ${fileId}:`, error) + window.modal.error({ content: t('files.delete.db_error'), centered: true }) // 提示数据库操作失败 + // Consider whether to attempt to restore the physical file (usually difficult) } } diff --git a/src/renderer/src/pages/history/HistoryPage.tsx b/src/renderer/src/pages/history/HistoryPage.tsx index cde7c34ba9..0ea5d2c481 100644 --- a/src/renderer/src/pages/history/HistoryPage.tsx +++ b/src/renderer/src/pages/history/HistoryPage.tsx @@ -1,5 +1,6 @@ import { ArrowLeftOutlined, EnterOutlined } from '@ant-design/icons' -import { Message, Topic } from '@renderer/types' +import { Topic } from '@renderer/types' +import type { Message } from '@renderer/types/newMessage' import { Input, InputRef } from 'antd' import { last } from 'lodash' import { Search } from 'lucide-react' diff --git a/src/renderer/src/pages/history/components/SearchMessage.tsx b/src/renderer/src/pages/history/components/SearchMessage.tsx index cc0c3663a1..b928486c28 100644 --- a/src/renderer/src/pages/history/components/SearchMessage.tsx +++ b/src/renderer/src/pages/history/components/SearchMessage.tsx @@ -5,7 +5,8 @@ import { getTopicById } from '@renderer/hooks/useTopic' import { default as MessageItem } from '@renderer/pages/home/Messages/Message' import { locateToMessage } from '@renderer/services/MessagesService' import NavigationService from '@renderer/services/NavigationService' -import { Message, Topic } from '@renderer/types' +import { Topic } from '@renderer/types' +import type { Message } from '@renderer/types/newMessage' import { runAsyncFunction } from '@renderer/utils' import { Button } from 'antd' import { FC, useEffect, useState } from 'react' diff --git a/src/renderer/src/pages/history/components/SearchResults.tsx b/src/renderer/src/pages/history/components/SearchResults.tsx index f469a57a28..60300adfed 100644 --- a/src/renderer/src/pages/history/components/SearchResults.tsx +++ b/src/renderer/src/pages/history/components/SearchResults.tsx @@ -1,7 +1,9 @@ import db from '@renderer/databases' import useScrollPosition from '@renderer/hooks/useScrollPosition' import { getTopicById } from '@renderer/hooks/useTopic' -import { Message, Topic } from '@renderer/types' +import { Topic } from '@renderer/types' +import type { Message } from '@renderer/types/newMessage' +import { getMainTextContent } from '@renderer/utils/messageUtils/find' import { List, Typography } from 'antd' import { useLiveQuery } from 'dexie-react-hooks' import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react' @@ -63,7 +65,8 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p .filter((term) => term.length > 0) for (const message of messages) { - const cleanContent = removeMarkdown(message.content.toLowerCase()) + const content = getMainTextContent(message) + const cleanContent = removeMarkdown(content.toLowerCase()) if (newSearchTerms.every((term) => cleanContent.includes(term))) { results.push({ message, topic: await getTopicById(message.topicId)! }) } @@ -124,7 +127,7 @@ const SearchResults: FC = ({ keywords, onMessageClick, onTopicClick, ...p {topic.name}
onMessageClick(message)}> - {highlightText(message.content)} + {highlightText(getMainTextContent(message))}
{new Date(message.createdAt).toLocaleString()} diff --git a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx index 765ce47526..4c7c470e1a 100644 --- a/src/renderer/src/pages/home/Inputbar/Inputbar.tsx +++ b/src/renderer/src/pages/home/Inputbar/Inputbar.tsx @@ -20,9 +20,10 @@ import { estimateMessageUsage, estimateTextTokens as estimateTxtTokens } from '@ import { translateText } from '@renderer/services/TranslateService' import WebSearchService from '@renderer/services/WebSearchService' import { useAppDispatch } from '@renderer/store' -import { sendMessage as _sendMessage } from '@renderer/store/messages' import { setSearching } from '@renderer/store/runtime' -import { Assistant, FileType, KnowledgeBase, KnowledgeItem, Message, Model, Topic } from '@renderer/types' +import { sendMessage as _sendMessage } from '@renderer/store/thunk/messageThunk' +import { Assistant, FileType, KnowledgeBase, KnowledgeItem, Model, Topic } from '@renderer/types' +import type { MessageInputBaseParams } from '@renderer/types/newMessage' import { classNames, delay, formatFileSize, getFileExtension } from '@renderer/utils' import { getFilesFromDropEvent } from '@renderer/utils/input' import { documentExts, imageExts, textExts } from '@shared/config/constant' @@ -47,6 +48,7 @@ import { Upload, Zap } from 'lucide-react' +// import { CompletionUsage } from 'openai/resources' import React, { CSSProperties, FC, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router-dom' @@ -174,41 +176,45 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = return } + console.log('[DEBUG] Starting to send message') + EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE) try { // Dispatch the sendMessage action with all options const uploadedFiles = await FileManager.uploadFiles(files) - const userMessage = getUserMessage({ assistant, topic, type: 'text', content: text }) + const baseUserMessage: MessageInputBaseParams = { assistant, topic, content: text } + + // getUserMessage() if (uploadedFiles) { - userMessage.files = uploadedFiles + baseUserMessage.files = uploadedFiles } - const knowledgeBaseIds = selectedKnowledgeBases?.map((base) => base.id) if (knowledgeBaseIds) { - userMessage.knowledgeBaseIds = knowledgeBaseIds + baseUserMessage.knowledgeBaseIds = knowledgeBaseIds } if (mentionModels) { - userMessage.mentions = mentionModels + baseUserMessage.mentions = mentionModels } if (!isEmpty(assistant.mcpServers) && !isEmpty(activedMcpServers)) { - userMessage.enabledMCPs = activedMcpServers.filter((server) => + baseUserMessage.enabledMCPs = activedMcpServers.filter((server) => assistant.mcpServers?.some((s) => s.id === server.id) ) } - userMessage.usage = await estimateMessageUsage(userMessage) - currentMessageId.current = userMessage.id + baseUserMessage.usage = await estimateMessageUsage(baseUserMessage) - dispatch( - _sendMessage(userMessage, assistant, topic, { - mentions: mentionModels - }) - ) + const { message, blocks } = getUserMessage(baseUserMessage) + + currentMessageId.current = message.id + console.log('[DEBUG] Created message and blocks:', message, blocks) + console.log('[DEBUG] Dispatching _sendMessage') + dispatch(_sendMessage(message, blocks, assistant, topic.id)) + console.log('[DEBUG] _sendMessage dispatched') // Clear input setText('') @@ -694,11 +700,11 @@ const Inputbar: FC = ({ assistant: _assistant, setActiveTopic, topic }) = useEffect(() => { const _setEstimateTokenCount = debounce(setEstimateTokenCount, 100, { leading: false, trailing: true }) const unsubscribes = [ - EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => { - setText(message.content) - textareaRef.current?.focus() - setTimeout(() => resizeTextArea(), 0) - }), + // EventEmitter.on(EVENT_NAMES.EDIT_MESSAGE, (message: Message) => { + // setText(message.content) + // textareaRef.current?.focus() + // setTimeout(() => resizeTextArea(), 0) + // }), EventEmitter.on(EVENT_NAMES.ESTIMATED_TOKEN_COUNT, ({ tokensCount, contextCount }) => { _setEstimateTokenCount(tokensCount) setContextCount({ current: contextCount.current, max: contextCount.max }) // 现在contextCount是一个对象而不是单个数值 diff --git a/src/renderer/src/pages/home/Markdown/Markdown.tsx b/src/renderer/src/pages/home/Markdown/Markdown.tsx index 60b83ec4ca..e3cc299784 100644 --- a/src/renderer/src/pages/home/Markdown/Markdown.tsx +++ b/src/renderer/src/pages/home/Markdown/Markdown.tsx @@ -4,9 +4,9 @@ import 'katex/dist/contrib/mhchem' import MarkdownShadowDOMRenderer from '@renderer/components/MarkdownShadowDOMRenderer' import { useSettings } from '@renderer/hooks/useSettings' -import type { Message } from '@renderer/types' +import type { MainTextMessageBlock, ThinkingMessageBlock, TranslationMessageBlock } from '@renderer/types/newMessage' import { parseJSON } from '@renderer/utils' -import { escapeBrackets, removeSvgEmptyLines, withGeminiGrounding } from '@renderer/utils/formats' +import { escapeBrackets, removeSvgEmptyLines } from '@renderer/utils/formats' import { findCitationInChildren } from '@renderer/utils/markdown' import { isEmpty } from 'lodash' import { type FC, useMemo } from 'react' @@ -29,12 +29,13 @@ const ALLOWED_ELEMENTS = const DISALLOWED_ELEMENTS = ['iframe'] interface Props { - message: Message + // message: Message & { content: string } + block: MainTextMessageBlock | TranslationMessageBlock | ThinkingMessageBlock } -const Markdown: FC = ({ message }) => { +const Markdown: FC = ({ block }) => { const { t } = useTranslation() - const { renderInputMessageAsMarkdown, mathEngine } = useSettings() + const { mathEngine } = useSettings() const remarkPlugins = useMemo(() => { const plugins = [remarkGfm, remarkCjkFriendly] @@ -45,11 +46,11 @@ const Markdown: FC = ({ message }) => { }, [mathEngine]) const messageContent = useMemo(() => { - const empty = isEmpty(message.content) - const paused = message.status === 'paused' - const content = empty && paused ? t('message.chat.completion.paused') : withGeminiGrounding(message) + const empty = isEmpty(block.content) + const paused = block.status === 'paused' + const content = empty && paused ? t('message.chat.completion.paused') : block.content return removeSvgEmptyLines(escapeBrackets(content)) - }, [message, t]) + }, [block, t]) const rehypePlugins = useMemo(() => { const plugins: any[] = [] @@ -74,9 +75,9 @@ const Markdown: FC = ({ message }) => { return baseComponents }, []) - if (message.role === 'user' && !renderInputMessageAsMarkdown) { - return

{messageContent}

- } + // if (role === 'user' && !renderInputMessageAsMarkdown) { + // return

{messageContent}

+ // } if (messageContent.includes('