mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-04 03:40:33 +08:00
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:
parent
ecd7518505
commit
23d086a1d2
@ -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
|
||||
|
||||
@ -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 }))
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -335,7 +335,8 @@
|
||||
"title": "レンダリングエラー"
|
||||
},
|
||||
"user_message_not_found": "元のユーザーメッセージを見つけることができませんでした",
|
||||
"unknown": "不明なエラー"
|
||||
"unknown": "不明なエラー",
|
||||
"pause_placeholder": "応答を一時停止しました"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "アシスタント",
|
||||
|
||||
@ -335,7 +335,8 @@
|
||||
"title": "Ошибка рендеринга"
|
||||
},
|
||||
"user_message_not_found": "Не удалось найти исходное сообщение пользователя",
|
||||
"unknown": "Неизвестная ошибка"
|
||||
"unknown": "Неизвестная ошибка",
|
||||
"pause_placeholder": "Получение ответа приостановлено"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "Ассистент",
|
||||
|
||||
@ -335,7 +335,8 @@
|
||||
"title": "渲染错误"
|
||||
},
|
||||
"user_message_not_found": "无法找到原始用户消息",
|
||||
"unknown": "未知错误"
|
||||
"unknown": "未知错误",
|
||||
"pause_placeholder": "已中断"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "助手",
|
||||
|
||||
@ -335,7 +335,8 @@
|
||||
"title": "渲染錯誤"
|
||||
},
|
||||
"user_message_not_found": "無法找到原始用戶訊息",
|
||||
"unknown": "未知錯誤"
|
||||
"unknown": "未知錯誤",
|
||||
"pause_placeholder": "回應已暫停"
|
||||
},
|
||||
"export": {
|
||||
"assistant": "助手",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -26,7 +26,6 @@ const encodeHTML = (str: string): string => {
|
||||
interface Props {
|
||||
block: MainTextMessageBlock
|
||||
citationBlockId?: string
|
||||
model?: Model
|
||||
mentions?: Model[]
|
||||
role: Message['role']
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user