Fix/message block structure (#5536)

* refactor:  重构快捷助手

- 移除不必要的 `model` 属性,简化 `MessageContent` 和相关组件的参数传递。
- 更新 `MessageItem` 和 `MessageBlockRenderer` 以提高可读性和性能,确保消息内容的正确渲染。
- 修复 `fetchCitations` 中的潜在错误,确保引用数据的正确处理。
- 清理未使用的代码和注释,提升代码整洁性。

* refactor: 优化消息块处理和错误显示逻辑

- 在 `upgradeToV7` 函数中调整了消息块的创建顺序,以保持与旧版本的一致性。
- 更新了 `ErrorBlock` 组件,增强了错误信息的显示逻辑,支持更详细的 HTTP 错误状态处理。
- 在多个语言文件中添加了暂停占位符文本,提升了用户体验。
- 移除了未使用的代码和注释,提升了代码整洁性。

* refactor(useAssistant): optimize topic handling with useMemo

* feat(locales): add Russian translations for MinApp and MiniWindow components

- Introduced new translations for various UI elements in the MinApp and MiniWindow sections, enhancing the user experience for Russian-speaking users.
- Updated the HomeWindow component to streamline topic handling by directly accessing the default assistant's topics, improving code clarity and performance.

* refactor(message): remove loading state management from newMessage slice and streamline message loading logic

- Removed the loading state update for topics in the `newMessage` slice to simplify state management.
- Updated `loadTopicMessagesThunk` to eliminate unnecessary loading checks, enhancing clarity and performance during message loading.

---------

Co-authored-by: suyao <sy20010504@gmail.com>
This commit is contained in:
MyPrototypeWhat 2025-04-30 17:58:08 +08:00 committed by GitHub
parent ecd7518505
commit 23d086a1d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 200 additions and 185 deletions

View File

@ -110,6 +110,37 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
const citationDataToCreate: Partial<Omit<CitationMessageBlock, keyof BaseMessageBlock | 'type'>> = {}
let hasCitationData = false
// 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)
}
// 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)
})
}
// 1. Main Text Block
if (oldMessage.content?.trim()) {
const block = createMainTextBlock(oldMessage.id, oldMessage.content, {
@ -121,16 +152,6 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
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', {
@ -177,25 +198,6 @@ export async function upgradeToV7(tx: Transaction): Promise<void> {
// 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

View File

@ -17,7 +17,7 @@ import {
} from '@renderer/store/assistants'
import { setDefaultModel, setTopicNamingModel, setTranslateModel } from '@renderer/store/llm'
import { Assistant, AssistantSettings, Model, Topic } from '@renderer/types'
import { useCallback } from 'react'
import { useCallback, useMemo } from 'react'
import { TopicManager } from './useTopic'
@ -84,11 +84,12 @@ export function useAssistant(id: string) {
export function useDefaultAssistant() {
const defaultAssistant = useAppSelector((state) => state.assistants.defaultAssistant)
const dispatch = useAppDispatch()
const memoizedTopics = useMemo(() => [getDefaultTopic(defaultAssistant.id)], [defaultAssistant.id])
return {
defaultAssistant: {
...defaultAssistant,
topics: [getDefaultTopic(defaultAssistant.id)]
topics: memoizedTopics
},
updateDefaultAssistant: (assistant: Assistant) => dispatch(updateDefaultAssistant({ assistant }))
}

View File

@ -335,7 +335,8 @@
"title": "Render Error"
},
"user_message_not_found": "Cannot find original user message to resend",
"unknown": "Unknown error"
"unknown": "Unknown error",
"pause_placeholder": "Paused"
},
"export": {
"assistant": "Assistant",

View File

@ -335,7 +335,8 @@
"title": "レンダリングエラー"
},
"user_message_not_found": "元のユーザーメッセージを見つけることができませんでした",
"unknown": "不明なエラー"
"unknown": "不明なエラー",
"pause_placeholder": "応答を一時停止しました"
},
"export": {
"assistant": "アシスタント",

View File

@ -335,7 +335,8 @@
"title": "Ошибка рендеринга"
},
"user_message_not_found": "Не удалось найти исходное сообщение пользователя",
"unknown": "Неизвестная ошибка"
"unknown": "Неизвестная ошибка",
"pause_placeholder": "Получение ответа приостановлено"
},
"export": {
"assistant": "Ассистент",

View File

@ -335,7 +335,8 @@
"title": "渲染错误"
},
"user_message_not_found": "无法找到原始用户消息",
"unknown": "未知错误"
"unknown": "未知错误",
"pause_placeholder": "已中断"
},
"export": {
"assistant": "助手",

View File

@ -335,7 +335,8 @@
"title": "渲染錯誤"
},
"user_message_not_found": "無法找到原始用戶訊息",
"unknown": "未知錯誤"
"unknown": "未知錯誤",
"pause_placeholder": "回應已暫停"
},
"export": {
"assistant": "助手",

View File

@ -1,14 +1,35 @@
import type { ErrorMessageBlock } from '@renderer/types/newMessage'
import { Alert as AntdAlert } from 'antd'
import React from 'react'
import MessageError from '../MessageError'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
interface Props {
block: ErrorMessageBlock
}
const ErrorBlock: React.FC<Props> = ({ block }) => {
return <MessageError block={block} />
return <MessageErrorInfo block={block} />
}
const MessageErrorInfo: React.FC<{ block: ErrorMessageBlock }> = ({ block }) => {
const { t, i18n } = useTranslation()
const HTTP_ERROR_CODES = [400, 401, 403, 404, 429, 500, 502, 503, 504]
if (block.error && HTTP_ERROR_CODES.includes(block.error?.status)) {
return <Alert description={t(`error.http.${block.error.status}`)} type="error" />
}
if (block?.error?.message) {
const errorKey = `error.${block.error.message}`
const pauseErrorLanguagePlaceholder = i18n.exists(errorKey) ? t(errorKey) : block.error.message
return <Alert description={pauseErrorLanguagePlaceholder} type="error" />
}
return <Alert description={t('error.chat.response')} type="error" />
}
const Alert = styled(AntdAlert)`
margin: 15px 0 8px;
padding: 10px;
font-size: 12px;
`
export default React.memo(ErrorBlock)

View File

@ -26,7 +26,6 @@ const encodeHTML = (str: string): string => {
interface Props {
block: MainTextMessageBlock
citationBlockId?: string
model?: Model
mentions?: Model[]
role: Message['role']
}

View File

@ -1,6 +1,5 @@
import type { RootState } from '@renderer/store'
import { messageBlocksSelectors } from '@renderer/store/messageBlock'
import type { Model } from '@renderer/types'
import type {
ErrorMessageBlock,
FileMessageBlock,
@ -28,16 +27,13 @@ import TranslationBlock from './TranslationBlock'
interface Props {
blocks: MessageBlock[] | string[] // 可以接收块ID数组或MessageBlock数组
model?: Model
messageStatus?: Message['status']
message: Message
}
const MessageBlockRenderer: React.FC<Props> = ({ blocks, model, message }) => {
const MessageBlockRenderer: React.FC<Props> = ({ blocks, message }) => {
// 始终调用useSelector避免条件调用Hook
const blockEntities = useSelector((state: RootState) => messageBlocksSelectors.selectEntities(state))
// if (!blocks || blocks.length === 0) return null
// 根据blocks类型处理渲染数据
const renderedBlocks = blocks.map((blockId) => blockEntities[blockId]).filter(Boolean)
return (
@ -61,7 +57,6 @@ const MessageBlockRenderer: React.FC<Props> = ({ blocks, model, message }) => {
<MainTextBlock
key={block.id}
block={mainTextBlock}
model={model}
// Pass only the ID string
citationBlockId={citationBlockId}
role={message.role}

View File

@ -146,7 +146,7 @@ const MessageItem: FC<Props> = ({
className="message-content-container"
style={{ fontFamily, fontSize, background: messageBackground, overflowY: 'visible' }}>
<MessageErrorBoundary>
<MessageContent message={message} model={model} />
<MessageContent message={message} />
</MessageErrorBoundary>
{showMenubar && (
<MessageFooter

View File

@ -1,5 +1,4 @@
import { getModelUniqId } from '@renderer/services/ModelService'
import { Model } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { Flex } from 'antd'
import React from 'react'
@ -8,10 +7,9 @@ import styled from 'styled-components'
import MessageBlockRenderer from './Blocks'
interface Props {
message: Message
model?: Model
}
const MessageContent: React.FC<Props> = ({ message, model }) => {
const MessageContent: React.FC<Props> = ({ message }) => {
// const { t } = useTranslation()
// if (message.status === 'pending') {
// return (
@ -46,7 +44,7 @@ const MessageContent: React.FC<Props> = ({ message, model }) => {
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex>
<MessageBlockRenderer blocks={message.blocks} model={model} message={message} />
<MessageBlockRenderer blocks={message.blocks} message={message} />
</>
)
}

View File

@ -154,7 +154,7 @@ const formatCitationsFromBlock = (block: CitationMessageBlock | undefined): Cita
break
case WebSearchSource.WEBSEARCH:
formattedCitations =
(block.response.results as WebSearchProviderResponse)?.results.map((result, index) => ({
(block.response.results as WebSearchProviderResponse)?.results?.map((result, index) => ({
number: index + 1,
url: result.url,
title: result.title,

View File

@ -82,7 +82,6 @@ const messagesSlice = createSlice({
const { topicId, messages } = action.payload
messagesAdapter.upsertMany(state, messages)
state.messageIdsByTopic[topicId] = messages.map((m) => m.id)
state.loadingByTopic[topicId] = false
},
addMessage(state, action: PayloadAction<{ topicId: string; message: Message }>) {
const { topicId, message } = action.payload

View File

@ -17,6 +17,7 @@ import type {
} from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus, MessageBlockType } from '@renderer/types/newMessage'
import { Response } from '@renderer/types/newMessage'
import { isAbortError } from '@renderer/utils/error'
import { extractUrlsFromMarkdown } from '@renderer/utils/linkConverter'
import {
createAssistantMessage,
@ -556,12 +557,19 @@ const fetchAndProcessAssistantResponseImpl = async (
}
},
onError: (error) => {
console.error('Stream processing error:', error)
console.dir(error, { depth: null })
let pauseErrorLanguagePlaceholder = ''
if (isAbortError(error)) {
pauseErrorLanguagePlaceholder = 'pause_placeholder'
}
const serializableError = {
name: error.name,
message: error.message || 'Stream processing error',
message: pauseErrorLanguagePlaceholder || error.message || 'Stream processing error',
originalMessage: error.message,
stack: error.stack
stack: error.stack,
status: error.status,
requestId: error.request_id
}
if (lastBlockId) {
// 更改上一个block的状态为ERROR
@ -705,17 +713,11 @@ export const loadTopicMessagesThunk =
async (dispatch: AppDispatch, getState: () => RootState) => {
const state = getState()
const topicMessagesExist = !!state.messages.messageIdsByTopic[topicId]
const isLoading = state.messages.loadingByTopic[topicId]
if ((topicMessagesExist && !forceReload) || isLoading) {
if (topicMessagesExist && isLoading) {
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
}
if (topicMessagesExist && !forceReload) {
return
}
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: true }))
dispatch(newMessagesActions.setCurrentTopicId(topicId))
try {
const topic = await db.topics.get(topicId)
const messagesFromDB = topic?.messages || []
@ -737,7 +739,7 @@ export const loadTopicMessagesThunk =
}
} catch (error: any) {
console.error(`[loadTopicMessagesThunk] Failed to load messages for topic ${topicId}:`, error)
dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
// dispatch(newMessagesActions.setTopicLoading({ topicId, loading: false }))
}
}

View File

@ -1,21 +1,21 @@
import Scrollbar from '@renderer/components/Scrollbar'
import { useDefaultAssistant } from '@renderer/hooks/useAssistant'
import { getDefaultModel } from '@renderer/services/AssistantService'
import { Assistant } from '@renderer/types'
import { FC } from 'react'
import styled from 'styled-components'
import Messages from './components/Messages'
interface Props {
route: string
assistant: Assistant
}
const ChatWindow: FC<Props> = ({ route }) => {
const { defaultAssistant } = useDefaultAssistant()
const ChatWindow: FC<Props> = ({ route, assistant }) => {
// const { defaultAssistant } = useDefaultAssistant()
return (
<Main className="bubble">
<Messages assistant={{ ...defaultAssistant, model: getDefaultModel() }} route={route} />
<Messages assistant={{ ...assistant, model: getDefaultModel() }} route={route} />
</Main>
)
}

View File

@ -1,18 +1,12 @@
import { FONT_FAMILY } from '@renderer/config/constant'
import { useModel } from '@renderer/hooks/useModel'
import { useSettings } from '@renderer/hooks/useSettings'
// import MessageContent from './MessageContent'
import MessageContent from '@renderer/pages/home/Messages/MessageContent'
import MessageErrorBoundary from '@renderer/pages/home/Messages/MessageErrorBoundary'
import { fetchChatCompletion } from '@renderer/services/ApiService'
import { getDefaultAssistant, getDefaultModel } from '@renderer/services/AssistantService'
import { getMessageModelId } from '@renderer/services/MessagesService'
import { Chunk, ChunkType } from '@renderer/types/chunk'
// import { LegacyMessage } from '@renderer/types'
import type { MainTextMessageBlock, Message } from '@renderer/types/newMessage'
import { AssistantMessageStatus, MessageBlockStatus } from '@renderer/types/newMessage'
import type { Message } from '@renderer/types/newMessage'
import { isMiniWindow } from '@renderer/utils'
import { createAssistantMessage, createMainTextBlock } from '@renderer/utils/messageUtils/create'
import { Dispatch, FC, memo, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
import { FC, memo, useMemo, useRef } from 'react'
import styled from 'styled-components'
interface Props {
@ -20,17 +14,15 @@ interface Props {
index?: number
total: number
route: string
onGetMessages?: () => Message[]
onSetMessages?: Dispatch<SetStateAction<Message[]>>
}
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
isBubbleStyle ? (isAssistantMessage ? 'transparent' : 'var(--chat-background-user)') : undefined
const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetMessages, onGetMessages }) => {
const [message, setMessage] = useState(_message)
const [textBlock, setTextBlock] = useState<MainTextMessageBlock | null>(null)
const model = useModel(getMessageModelId(message))
const MessageItem: FC<Props> = ({ message, index, total, route }) => {
// const [message, setMessage] = useState(_message)
// const [bl, setTextBlock] = useState<MainTextMessageBlock | null>(null)
// const model = useModel(getMessageModelId(message))
const isBubbleStyle = true
const { messageFont, fontSize } = useSettings()
const messageContainerRef = useRef<HTMLDivElement>(null)
@ -45,43 +37,6 @@ const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetM
const maxWidth = isMiniWindow() ? '800px' : '100%'
useEffect(() => {
if (onGetMessages && onSetMessages) {
if (message.status === AssistantMessageStatus.PROCESSING) {
const messages = onGetMessages()
const assistant = getDefaultAssistant()
fetchChatCompletion({
messages: messages
.filter((m) => !m.status.includes('ing'))
.slice(
0,
messages.findIndex((m) => m.id === message.id)
),
assistant: { ...assistant, model: getDefaultModel() },
onChunkReceived: (chunk: Chunk) => {
if (chunk.type === ChunkType.TEXT_DELTA) {
if (!textBlock) {
const block = createMainTextBlock(message.id, chunk.text, { status: MessageBlockStatus.STREAMING })
const assistantMessage = createAssistantMessage(assistant.id, message.topicId, {
blocks: [block.id]
})
setTextBlock(block)
setMessage(assistantMessage)
} else {
setTextBlock((prev) => {
if (prev) {
return { ...prev, content: (prev?.content ?? '') + chunk.text }
}
return null
})
}
}
}
})
}
}
}, [message.status, message.topicId, textBlock, message.id, onGetMessages, onSetMessages])
if (['summary', 'explanation'].includes(route) && index === total - 1) {
return null
}
@ -100,7 +55,7 @@ const MessageItem: FC<Props> = ({ message: _message, index, total, route, onSetM
...(isAssistantMessage ? { paddingLeft: 5, paddingRight: 5 } : {})
}}>
<MessageErrorBoundary>
<MessageContent message={message} model={model} />
<MessageContent message={message} />
</MessageErrorBoundary>
</MessageContentContainer>
</MessageContainer>

View File

@ -1,28 +1,25 @@
import Markdown from '@renderer/pages/home/Markdown/Markdown'
import { getModelUniqId } from '@renderer/services/ModelService'
import type { MainTextMessageBlock, Message } from '@renderer/types/newMessage'
import { Flex } from 'antd'
import type { MainTextMessageBlock } from '@renderer/types/newMessage'
import React from 'react'
import styled from 'styled-components'
interface Props {
message: Message
block: MainTextMessageBlock
}
const MessageContent: React.FC<Props> = ({ message, block }) => {
const MessageContent: React.FC<Props> = ({ block }) => {
console.log('block', block)
return (
<>
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{/* <Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={getModelUniqId(model)}>{'@' + model.name}</MentionTag>)}
</Flex>
</Flex> */}
<Markdown block={block} />
</>
)
}
const MentionTag = styled.span`
color: var(--color-link);
`
// const MentionTag = styled.span`
// color: var(--color-link);
// `
export default React.memo(MessageContent)

View File

@ -1,11 +1,9 @@
import Scrollbar from '@renderer/components/Scrollbar'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { getAssistantMessage } from '@renderer/services/MessagesService'
import { useTopicMessages } from '@renderer/hooks/useMessageOperations'
import { Assistant } from '@renderer/types'
import type { Message } from '@renderer/types/newMessage'
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
import { last } from 'lodash'
import { FC, useCallback, useEffect, useRef, useState } from 'react'
import { FC, useRef } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { useTranslation } from 'react-i18next'
import styled from 'styled-components'
@ -21,8 +19,8 @@ interface ContainerProps {
}
const Messages: FC<Props> = ({ assistant, route }) => {
const [messages, setMessages] = useState<Message[]>([])
// const [messages, setMessages] = useState<Message[]>([])
const messages = useTopicMessages(assistant.topics[0])
const containerRef = useRef<HTMLDivElement>(null)
const messagesRef = useRef(messages)
@ -30,25 +28,23 @@ const Messages: FC<Props> = ({ assistant, route }) => {
messagesRef.current = messages
const onSendMessage = useCallback(
async (message: Message) => {
setMessages((prev) => {
const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
const messages = prev.concat([message, assistantMessage])
return messages
})
},
[assistant]
)
// const onSendMessage = useCallback(
// async (message: Message) => {
// setMessages((prev) => {
// const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
// store.dispatch(newMessagesActions.addMessage({ topicId: assistant.topics[0].id, message: assistantMessage }))
const onGetMessages = useCallback(() => {
return messagesRef.current
}, [])
// const messages = prev.concat([message, assistantMessage])
// return messages
// })
// },
// [assistant]
// )
useEffect(() => {
const unsubscribes = [EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)]
return () => unsubscribes.forEach((unsub) => unsub())
}, [assistant.id, onSendMessage])
// useEffect(() => {
// const unsubscribes = [EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage)]
// return () => unsubscribes.forEach((unsub) => unsub())
// }, [assistant.id])
useHotkeys('c', () => {
const lastMessage = last(messages)
@ -58,19 +54,10 @@ const Messages: FC<Props> = ({ assistant, route }) => {
window.message.success(t('message.copy.success'))
}
})
return (
<Container id="messages" key={assistant.id} ref={containerRef}>
{[...messages].reverse().map((message, index) => (
<MessageItem
key={message.id}
message={message}
index={index}
total={messages.length}
onSetMessages={setMessages}
onGetMessages={onGetMessages}
route={route}
/>
<MessageItem key={message.id} message={message} index={index} total={messages.length} route={route} />
))}
</Container>
)

View File

@ -2,8 +2,17 @@ import { isMac } from '@renderer/config/constant'
import { useDefaultAssistant, useDefaultModel } from '@renderer/hooks/useAssistant'
import { useSettings } from '@renderer/hooks/useSettings'
import i18n from '@renderer/i18n'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { uuid } from '@renderer/utils'
import { fetchChatCompletion } from '@renderer/services/ApiService'
import { getDefaultAssistant, getDefaultModel } from '@renderer/services/AssistantService'
import { getAssistantMessage, getUserMessage } from '@renderer/services/MessagesService'
import store from '@renderer/store'
import { upsertManyBlocks } from '@renderer/store/messageBlock'
import { updateOneBlock, upsertOneBlock } from '@renderer/store/messageBlock'
import { newMessagesActions } from '@renderer/store/newMessage'
import { Chunk, ChunkType } from '@renderer/types/chunk'
import { AssistantMessageStatus } from '@renderer/types/newMessage'
import { MessageBlockStatus } from '@renderer/types/newMessage'
import { createMainTextBlock } from '@renderer/utils/messageUtils/create'
import { defaultLanguage } from '@shared/config/constant'
import { IpcChannel } from '@shared/IpcChannel'
import { Divider } from 'antd'
@ -30,12 +39,12 @@ const HomeWindow: FC = () => {
const [lastClipboardText, setLastClipboardText] = useState<string | null>(null)
const textChange = useState(() => {})[1]
const { defaultAssistant } = useDefaultAssistant()
const topic = defaultAssistant.topics[0]
const { defaultModel: model } = useDefaultModel()
const { language, readClipboardAtStartup, windowStyle, theme } = useSettings()
const { t } = useTranslation()
const inputBarRef = useRef<HTMLDivElement>(null)
const featureMenusRef = useRef<FeatureMenusRef>(null)
const referenceText = selectedText || clipboardText || text
const content = isFirstMessage ? (referenceText === text ? text : `${referenceText}\n\n${text}`).trim() : text.trim()
@ -147,23 +156,68 @@ const HomeWindow: FC = () => {
return
}
setTimeout(() => {
const message = {
id: uuid(),
role: 'user',
content: prompt ? `${prompt}\n\n${content}` : content,
assistantId: defaultAssistant.id,
topicId: defaultAssistant.topics[0].id || uuid(),
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
type: 'text',
status: 'success'
const messageParams = {
role: 'user',
content: prompt ? `${prompt}\n\n${content}` : content,
assistant: defaultAssistant,
topic,
createdAt: dayjs().format('YYYY-MM-DD HH:mm:ss'),
status: 'success'
}
const topicId = topic.id
const { message: userMessage, blocks } = getUserMessage(messageParams)
store.dispatch(newMessagesActions.addMessage({ topicId, message: userMessage }))
store.dispatch(upsertManyBlocks(blocks))
const assistant = getDefaultAssistant()
let blockId: string | null = null
let blockContent: string = ''
const assistantMessage = getAssistantMessage({ assistant, topic: assistant.topics[0] })
store.dispatch(newMessagesActions.addMessage({ topicId, message: assistantMessage }))
fetchChatCompletion({
messages: [userMessage],
assistant: { ...assistant, model: getDefaultModel() },
onChunkReceived: (chunk: Chunk) => {
console.log('chunk', chunk)
if (chunk.type === ChunkType.TEXT_DELTA) {
blockContent += chunk.text
if (!blockId) {
const block = createMainTextBlock(assistantMessage.id, chunk.text, {
status: MessageBlockStatus.STREAMING
})
blockId = block.id
store.dispatch(
newMessagesActions.updateMessage({
topicId,
messageId: assistantMessage.id,
updates: { blockInstruction: { id: block.id } }
})
)
store.dispatch(upsertOneBlock(block))
} else {
store.dispatch(updateOneBlock({ id: blockId, changes: { content: blockContent } }))
}
}
if (chunk.type === ChunkType.TEXT_COMPLETE) {
blockId && store.dispatch(updateOneBlock({ id: blockId, changes: { status: MessageBlockStatus.SUCCESS } }))
store.dispatch(
newMessagesActions.updateMessage({
topicId,
messageId: assistantMessage.id,
updates: { status: AssistantMessageStatus.SUCCESS }
})
)
}
}
EventEmitter.emit(EVENT_NAMES.SEND_MESSAGE, message)
setIsFirstMessage(false)
setText('') // ✅ 清除输入框内容
}, 0)
})
setIsFirstMessage(false)
setText('') // ✅ 清除输入框内容
},
[content, defaultAssistant.id, defaultAssistant.topics]
[content, defaultAssistant]
)
const clearClipboard = () => {
@ -240,7 +294,7 @@ const HomeWindow: FC = () => {
<ClipboardPreview referenceText={referenceText} clearClipboard={clearClipboard} t={t} />
</div>
)}
<ChatWindow route={route} />
<ChatWindow route={route} assistant={defaultAssistant} />
<Divider style={{ margin: '10px 0' }} />
<Footer route={route} onExit={() => setRoute('home')} />
</Container>