mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-31 08:29:07 +08:00
feat: context message in message group (#8833)
* stash * docs(newMessage): 修正注释中的拼写错误 * refactor(MessageGroup): 优化组件逻辑和状态管理 重构消息组件的状态管理和逻辑顺序,提升代码可读性 将相关状态和逻辑分组,并提取公共变量 * feat(消息组件): 添加消息有用性更新功能 在MessageGroup组件中实现onUpdateUseful回调,用于更新消息的有用状态 当标记某条消息为有用时,自动取消其他消息的有用标记 * fix(i18n): 更新多语言翻译文件中的键值 - 将中文简体中的"useful"键值从"有用"改为"设置为上下文" - 在其他语言文件中为"useful"键添加待翻译标记 - 在部分语言文件中添加"merge"、"longRunning"等新键的待翻译标记 * feat(消息组): 添加群组上下文消息标识和有用消息提示 为消息组添加上下文消息标识功能,当消息被标记为有用时显示特殊标识 优化消息菜单栏的有用按钮提示文本 修复消息菜单栏依赖项数组不完整的问题 * feat(i18n): 更新多语言翻译文件并改进自动翻译脚本 为"useful"字段添加label和tip翻译,完善多个语言的翻译内容 改进自动翻译脚本,使用语言映射替换文件名 * docs(i18n): 更新多语言文件中上下文提示的翻译文本 * docs(messageUtils): 标记废弃工具调用结果消息构造函数 标记 `构造带工具调用结果的消息内容` 函数为废弃状态,后续将移除 * refactor(消息过滤): 重命名filterContextMessages为filterAfterContextClearMessages以更准确描述功能 * fix(MessageGroup): 修复依赖数组中缺少groupContextMessageId的问题 * feat(消息过滤): 添加根据上下文数量过滤消息的功能 * refactor(消息过滤): 拆分消息过滤逻辑并添加日志 将filterUsefulMessages函数拆分为多个独立函数,提高代码可维护性 添加日志输出以便调试消息过滤过程 * refactor(消息过滤): 优化聊天消息过滤逻辑并添加调试日志 重构消息过滤流程,将原有单步过滤拆分为多步处理 添加调试日志以跟踪各阶段过滤结果 * refactor(messageUtils): 移除未使用的logger并优化消息过滤逻辑 移除未使用的logger导入和调用,添加filterAdjacentUserMessaegs过滤步骤优化消息处理流程 * refactor(消息服务): 重构获取上下文消息数量的逻辑 使用 filterContextMessages 工具函数替代 lodash 的 takeRight 和手动计算逻辑 * fix(消息工具): 修复分组消息排序顺序错误 * fix(消息过滤): 优化消息组过滤逻辑,保留有用消息或最后一条消息 修改 filterUsefulMessages 函数注释以更清晰说明过滤逻辑 在 MessageGroup 组件中使用 lodash 的 last 方法获取最后一条消息 * fix(MessageGroup): 修复消息有用性更新逻辑的错误 处理消息有用性状态更新时,添加对消息存在性的检查并优化状态切换逻辑 * fix(Messages): 修复分组消息内部顺序不正确的问题 由于displayMessages是倒序的,导致分组后的消息内部顺序也是倒序的。通过toReversed()将每个分组内部的消息顺序再次反转,确保正确显示 * fix(消息过滤): 修改未标记有用消息的保留策略,从保留最后一条改为第一条 * fix: 将onUpdateUseful属性改为可选以处理未定义情况 * refactor(ApiService): 移除冗余的日志记录调用 * docs(types): 去除Message类型中useful字段的过时注释 * refactor(messageUtils): 移除分组消息中的冗余排序操作 原代码在分组消息时已经按原始索引顺序添加,无需再次排序
This commit is contained in:
parent
6b8ba9d273
commit
96a4c95a3a
@ -24,6 +24,17 @@ const openai = new OpenAI({
|
||||
baseURL: BASE_URL
|
||||
})
|
||||
|
||||
const languageMap = {
|
||||
'en-us': 'English',
|
||||
'ja-jp': 'Japanese',
|
||||
'ru-ru': 'Russian',
|
||||
'zh-tw': 'Traditional Chinese',
|
||||
'el-gr': 'Greek',
|
||||
'es-es': 'Spanish',
|
||||
'fr-fr': 'French',
|
||||
'pt-pt': 'Portuguese'
|
||||
}
|
||||
|
||||
const PROMPT = `
|
||||
You are a translation expert. Your sole responsibility is to translate the text enclosed within <translate_input> from the source language into {{target_language}}.
|
||||
Output only the translated text, preserving the original format, and without including any explanations, headers such as "TRANSLATE", or the <translate_input> tags.
|
||||
@ -117,7 +128,7 @@ const main = async () => {
|
||||
console.error(`解析 ${filename} 出错,跳过此文件。`, error)
|
||||
continue
|
||||
}
|
||||
const systemPrompt = PROMPT.replace('{{target_language}}', filename)
|
||||
const systemPrompt = PROMPT.replace('{{target_language}}', languageMap[filename])
|
||||
|
||||
const result = await translateRecursively(targetJson, systemPrompt)
|
||||
count += 1
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "Switch Model"
|
||||
},
|
||||
"useful": "Helpful"
|
||||
"useful": {
|
||||
"label": "Set as context",
|
||||
"tip": "In this group of messages, this message will be selected to join the context"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "モデルを切り替え"
|
||||
},
|
||||
"useful": "役立つ"
|
||||
"useful": {
|
||||
"label": "上下文として設定する",
|
||||
"tip": "このメッセージは、このメッセージセットの中でコンテキストに含まれるために選択されます"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "Переключить модель"
|
||||
},
|
||||
"useful": "Полезно"
|
||||
"useful": {
|
||||
"label": "установить в качестве контекста",
|
||||
"tip": "В этой группе сообщений данное сообщение будет выбрано для включения в контекст"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "切换模型"
|
||||
},
|
||||
"useful": "有用"
|
||||
"useful": {
|
||||
"label": "设置为上下文",
|
||||
"tip": "在这组消息中,该消息将被选择加入上下文"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "切換模型"
|
||||
},
|
||||
"useful": "有用"
|
||||
"useful": {
|
||||
"label": "設置為上下文",
|
||||
"tip": "在這組訊息中,該訊息將被選擇加入上下文"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "Εναλλαγή μοντέλου"
|
||||
},
|
||||
"useful": "Χρήσιμο"
|
||||
"useful": {
|
||||
"label": "Ορισμός ως πλαίσιο αναφοράς",
|
||||
"tip": "Σε αυτή την ομάδα μηνυμάτων, αυτό το μήνυμα θα επιλεγεί για να συμπεριληφθεί στο πλαίσιο"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "Cambiar modelo"
|
||||
},
|
||||
"useful": "Útil"
|
||||
"useful": {
|
||||
"label": "establecer como contexto",
|
||||
"tip": "En este grupo de mensajes, este mensaje se seleccionará para unirse al contexto"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "Changer de modèle"
|
||||
},
|
||||
"useful": "Utile"
|
||||
"useful": {
|
||||
"label": "Définir comme contexte",
|
||||
"tip": "Dans ce groupe de messages, ce message sera sélectionné pour être inclus dans le contexte"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
@ -2769,7 +2772,7 @@
|
||||
"jsonSaveSuccess": "Configuration JSON sauvegardée",
|
||||
"logoUrl": "Адрес логотипа",
|
||||
"longRunning": "Mode d'exécution prolongée",
|
||||
"longRunningTooltip": "Activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de délai d'attente lorsqu'il reçoit une notification de progression et prolonge le délai d'expiration maximal à 10 minutes.",
|
||||
"longRunningTooltip": "Une fois activé, le serveur prend en charge les tâches de longue durée, réinitialise le minuteur de temporisation à la réception des notifications de progression, et prolonge le délai d'expiration maximal à 10 minutes.",
|
||||
"missingDependencies": "Manquantes, veuillez les installer pour continuer",
|
||||
"more": {
|
||||
"awesome": "Liste sélectionnée de serveurs MCP",
|
||||
|
||||
@ -405,7 +405,10 @@
|
||||
"regenerate": {
|
||||
"model": "Trocar modelo"
|
||||
},
|
||||
"useful": "Útil"
|
||||
"useful": {
|
||||
"label": "Definido como contexto",
|
||||
"tip": "Neste conjunto de mensagens, esta mensagem será selecionada para ingressar no contexto"
|
||||
}
|
||||
},
|
||||
"multiple": {
|
||||
"select": {
|
||||
@ -2769,7 +2772,7 @@
|
||||
"jsonSaveSuccess": "Configuração JSON salva com sucesso",
|
||||
"logoUrl": "URL do Logotipo",
|
||||
"longRunning": "Modo de execução prolongada",
|
||||
"longRunningTooltip": "Ativado, o servidor suporta tarefas de longa duração, reiniciando o temporizador de tempo limite ao receber notificações de progresso e prolongando o tempo máximo de tempo limite para 10 minutos.",
|
||||
"longRunningTooltip": "Quando ativado, o servidor suporta tarefas de longa duração, redefinindo o temporizador de tempo limite ao receber notificações de progresso e estendendo o tempo máximo de tempo limite para 10 minutos.",
|
||||
"missingDependencies": "Ausente, instale para continuar",
|
||||
"more": {
|
||||
"awesome": "Lista selecionada de servidores MCP",
|
||||
|
||||
@ -35,6 +35,8 @@ interface Props {
|
||||
isGrouped?: boolean
|
||||
isStreaming?: boolean
|
||||
onSetMessages?: Dispatch<SetStateAction<Message[]>>
|
||||
onUpdateUseful?: (msgId: string) => void
|
||||
isGroupContextMessage?: boolean
|
||||
}
|
||||
|
||||
const logger = loggerService.withContext('MessageItem')
|
||||
@ -56,7 +58,9 @@ const MessageItem: FC<Props> = ({
|
||||
index,
|
||||
hideMenuBar = false,
|
||||
isGrouped,
|
||||
isStreaming = false
|
||||
isStreaming = false,
|
||||
onUpdateUseful,
|
||||
isGroupContextMessage
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { assistant, setModel } = useAssistant(message.assistantId)
|
||||
@ -166,6 +170,7 @@ const MessageItem: FC<Props> = ({
|
||||
model={model}
|
||||
key={getModelUniqId(model)}
|
||||
topic={topic}
|
||||
isGroupContextMessage={isGroupContextMessage}
|
||||
/>
|
||||
{isEditing && (
|
||||
<MessageEditor
|
||||
@ -202,6 +207,7 @@ const MessageItem: FC<Props> = ({
|
||||
isGrouped={isGrouped}
|
||||
messageContainerRef={messageContainerRef as React.RefObject<HTMLDivElement>}
|
||||
setModel={setModel}
|
||||
onUpdateUseful={onUpdateUseful}
|
||||
/>
|
||||
</MessageFooter>
|
||||
)}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { loggerService } from '@logger'
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { MessageEditingProvider } from '@renderer/context/MessageEditingContext'
|
||||
import { useChatContext } from '@renderer/hooks/useChatContext'
|
||||
@ -16,6 +17,7 @@ import { useChatMaxWidth } from '../Chat'
|
||||
import MessageItem from './Message'
|
||||
import MessageGroupMenuBar from './MessageGroupMenuBar'
|
||||
|
||||
const logger = loggerService.withContext('MessageGroup')
|
||||
interface Props {
|
||||
messages: (Message & { index: number })[]
|
||||
topic: Topic
|
||||
@ -23,14 +25,24 @@ interface Props {
|
||||
}
|
||||
|
||||
const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
const messageLength = messages.length
|
||||
|
||||
// Hooks
|
||||
const { editMessage } = useMessageOperations(topic)
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||
const { isMultiSelectMode } = useChatContext(topic)
|
||||
const messageLength = messages.length
|
||||
const maxWidth = useChatMaxWidth()
|
||||
|
||||
const isGrouped = isMultiSelectMode ? false : messageLength > 1 && messages.every((m) => m.role === 'assistant')
|
||||
|
||||
// States
|
||||
const [_multiModelMessageStyle, setMultiModelMessageStyle] = useState<MultiModelMessageStyle>(
|
||||
messages[0].multiModelMessageStyle || multiModelMessageStyleSetting
|
||||
)
|
||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||
|
||||
// Refs
|
||||
const prevMessageLengthRef = useRef(messageLength)
|
||||
|
||||
// 对于单模型消息,采用简单的样式,避免 overflow 影响内部的 sticky 效果
|
||||
const multiModelMessageStyle = useMemo(
|
||||
@ -38,8 +50,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
[_multiModelMessageStyle, messageLength]
|
||||
)
|
||||
|
||||
const prevMessageLengthRef = useRef(messageLength)
|
||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||
const isGrid = multiModelMessageStyle === 'grid'
|
||||
|
||||
const selectedMessageId = useMemo(() => {
|
||||
if (messages.length === 1) return messages[0]?.id
|
||||
@ -67,9 +78,6 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
[editMessage, selectedMessageId]
|
||||
)
|
||||
|
||||
const isGrouped = isMultiSelectMode ? false : messageLength > 1 && messages.every((m) => m.role === 'assistant')
|
||||
const isGrid = multiModelMessageStyle === 'grid'
|
||||
|
||||
useEffect(() => {
|
||||
if (messageLength > prevMessageLengthRef.current) {
|
||||
setSelectedIndex(messageLength - 1)
|
||||
@ -164,6 +172,43 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
return () => messages.forEach((message) => registerMessageElement?.(message.id, null))
|
||||
}, [messages, registerMessageElement])
|
||||
|
||||
const onUpdateUseful = useCallback(
|
||||
(msgId: string) => {
|
||||
const message = messages.find((msg) => msg.id === msgId)
|
||||
if (!message) {
|
||||
logger.error("the message to update doesn't exist in this group")
|
||||
return
|
||||
}
|
||||
if (message.useful) {
|
||||
editMessage(msgId, { useful: undefined })
|
||||
return
|
||||
} else {
|
||||
const toResetUsefulMsgs = messages.filter((msg) => msg.id !== msgId && msg.useful)
|
||||
toResetUsefulMsgs.forEach(async (msg) => {
|
||||
editMessage(msg.id, {
|
||||
useful: undefined
|
||||
})
|
||||
})
|
||||
editMessage(msgId, { useful: true })
|
||||
}
|
||||
},
|
||||
[editMessage, messages]
|
||||
)
|
||||
|
||||
const groupContextMessageId = useMemo(() => {
|
||||
// NOTE: 旧数据可能存在一组消息有多个useful的情况,只取第一个,不再另作迁移
|
||||
// find first useful
|
||||
const usefulMsg = messages.find((msg) => msg.useful)
|
||||
if (usefulMsg) {
|
||||
return usefulMsg.id
|
||||
} else if (messages.length > 0) {
|
||||
return messages[0].id
|
||||
} else {
|
||||
logger.warn('Empty message group')
|
||||
return ''
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
const renderMessage = useCallback(
|
||||
(message: Message & { index: number }) => {
|
||||
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
||||
@ -184,7 +229,11 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
selected: message.id === selectedMessageId
|
||||
}
|
||||
])}>
|
||||
<MessageItem {...messageProps} />
|
||||
<MessageItem
|
||||
onUpdateUseful={onUpdateUseful}
|
||||
isGroupContextMessage={isGrouped && message.id === groupContextMessageId}
|
||||
{...messageProps}
|
||||
/>
|
||||
</MessageWrapper>
|
||||
)
|
||||
|
||||
@ -202,7 +251,7 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
selected: message.id === selectedMessageId
|
||||
}
|
||||
])}>
|
||||
<MessageItem {...messageProps} />
|
||||
<MessageItem onUpdateUseful={onUpdateUseful} {...messageProps} />
|
||||
</MessageWrapper>
|
||||
}
|
||||
trigger={gridPopoverTrigger}
|
||||
@ -217,11 +266,19 @@ const MessageGroup = ({ messages, topic, registerMessageElement }: Props) => {
|
||||
|
||||
return messageContent
|
||||
},
|
||||
[isGrid, isGrouped, topic, multiModelMessageStyle, messages.length, selectedMessageId, gridPopoverTrigger]
|
||||
[
|
||||
isGrid,
|
||||
isGrouped,
|
||||
topic,
|
||||
multiModelMessageStyle,
|
||||
messages.length,
|
||||
selectedMessageId,
|
||||
onUpdateUseful,
|
||||
groupContextMessageId,
|
||||
gridPopoverTrigger
|
||||
]
|
||||
)
|
||||
|
||||
const maxWidth = useChatMaxWidth()
|
||||
|
||||
return (
|
||||
<MessageEditingProvider>
|
||||
<GroupContainer
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import EmojiAvatar from '@renderer/components/Avatar/EmojiAvatar'
|
||||
import { HStack } from '@renderer/components/Layout'
|
||||
import UserPopup from '@renderer/components/Popups/UserPopup'
|
||||
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
@ -12,8 +13,9 @@ import { getModelName } from '@renderer/services/ModelService'
|
||||
import type { Assistant, Model, Topic } from '@renderer/types'
|
||||
import type { Message } from '@renderer/types/newMessage'
|
||||
import { firstLetter, isEmoji, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar, Checkbox } from 'antd'
|
||||
import { Avatar, Checkbox, Tooltip } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { Sparkle } from 'lucide-react'
|
||||
import { FC, memo, useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
@ -23,6 +25,7 @@ interface Props {
|
||||
assistant: Assistant
|
||||
model?: Model
|
||||
topic: Topic
|
||||
isGroupContextMessage?: boolean
|
||||
}
|
||||
|
||||
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
|
||||
@ -30,7 +33,7 @@ const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
|
||||
return modelId ? getModelLogo(modelId) : undefined
|
||||
}
|
||||
|
||||
const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic }) => {
|
||||
const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic, isGroupContextMessage }) => {
|
||||
const avatar = useAvatar()
|
||||
const { theme } = useTheme()
|
||||
const { userName, sidebarIcons } = useSettings()
|
||||
@ -107,9 +110,16 @@ const MessageHeader: FC<Props> = memo(({ assistant, model, message, topic }) =>
|
||||
</>
|
||||
)}
|
||||
<UserWrap>
|
||||
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
|
||||
{username}
|
||||
</UserName>
|
||||
<HStack alignItems="center">
|
||||
<UserName isBubbleStyle={isBubbleStyle} theme={theme}>
|
||||
{username}
|
||||
</UserName>
|
||||
{isGroupContextMessage && (
|
||||
<Tooltip title={t('chat.message.useful.tip')}>
|
||||
<Sparkle fill="var(--color-primary)" strokeWidth={0} size={18} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</HStack>
|
||||
<InfoWrap className="message-header-info-wrap">
|
||||
<MessageTime>{dayjs(message?.updatedAt ?? message.createdAt).format('MM/DD HH:mm')}</MessageTime>
|
||||
</InfoWrap>
|
||||
@ -150,7 +160,7 @@ const InfoWrap = styled.div`
|
||||
gap: 4px;
|
||||
`
|
||||
|
||||
const UserName = styled.div<{ isBubbleStyle?: boolean; theme?: string }>`
|
||||
const UserName = styled.span<{ isBubbleStyle?: boolean; theme?: string }>`
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: ${(props) => (props.isBubbleStyle && props.theme === 'dark' ? 'white' : 'var(--color-text)')};
|
||||
|
||||
@ -52,11 +52,22 @@ interface Props {
|
||||
isAssistantMessage: boolean
|
||||
messageContainerRef: React.RefObject<HTMLDivElement>
|
||||
setModel: (model: Model) => void
|
||||
onUpdateUseful?: (msgId: string) => void
|
||||
}
|
||||
|
||||
const MessageMenubar: FC<Props> = (props) => {
|
||||
const { message, index, isGrouped, isLastMessage, isAssistantMessage, assistant, topic, model, messageContainerRef } =
|
||||
props
|
||||
const {
|
||||
message,
|
||||
index,
|
||||
isGrouped,
|
||||
isLastMessage,
|
||||
isAssistantMessage,
|
||||
assistant,
|
||||
topic,
|
||||
model,
|
||||
messageContainerRef,
|
||||
onUpdateUseful
|
||||
} = props
|
||||
const { t } = useTranslation()
|
||||
const { toggleMultiSelectMode } = useChatContext(props.topic)
|
||||
const [copied, setCopied] = useState(false)
|
||||
@ -65,7 +76,6 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const [showDeleteTooltip, setShowDeleteTooltip] = useState(false)
|
||||
// const assistantModel = assistant?.model
|
||||
const {
|
||||
editMessage,
|
||||
deleteMessage,
|
||||
resendMessage,
|
||||
regenerateAssistantMessage,
|
||||
@ -402,9 +412,9 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
const onUseful = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
editMessage(message.id, { useful: !message.useful })
|
||||
onUpdateUseful?.(message.id)
|
||||
},
|
||||
[message, editMessage]
|
||||
[message.id, onUpdateUseful]
|
||||
)
|
||||
|
||||
const blockEntities = useSelector(messageBlocksSelectors.selectEntities)
|
||||
@ -546,7 +556,7 @@ const MessageMenubar: FC<Props> = (props) => {
|
||||
</Dropdown>
|
||||
)}
|
||||
{isAssistantMessage && isGrouped && (
|
||||
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
|
||||
<Tooltip title={t('chat.message.useful.label')} mouseEnterDelay={0.8}>
|
||||
<ActionButton className="message-action-button" onClick={onUseful} $softHoverBg={softHoverBg}>
|
||||
{message.useful ? (
|
||||
<ThumbsUp size={17.5} fill="var(--color-primary)" strokeWidth={0} />
|
||||
|
||||
@ -278,7 +278,19 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic, o
|
||||
requestAnimationFrame(() => onComponentUpdate?.())
|
||||
}, [onComponentUpdate])
|
||||
|
||||
const groupedMessages = useMemo(() => Object.entries(getGroupedMessages(displayMessages)), [displayMessages])
|
||||
// NOTE: 因为displayMessages是倒序的,所以得到的groupedMessages每个group内部也是倒序的,需要再倒一遍
|
||||
const groupedMessages = useMemo(() => {
|
||||
const grouped = Object.entries(getGroupedMessages(displayMessages))
|
||||
const newGrouped: {
|
||||
[key: string]: (Message & {
|
||||
index: number
|
||||
})[]
|
||||
} = {}
|
||||
grouped.forEach(([key, group]) => {
|
||||
newGrouped[key] = group.toReversed()
|
||||
})
|
||||
return Object.entries(newGrouped)
|
||||
}, [displayMessages])
|
||||
|
||||
return (
|
||||
<MessagesContainer
|
||||
|
||||
@ -45,6 +45,7 @@ import {
|
||||
} from '@renderer/utils/abortController'
|
||||
import { isAbortError } from '@renderer/utils/error'
|
||||
import { extractInfoFromXML, ExtractResults } from '@renderer/utils/extract'
|
||||
import { filterAdjacentUserMessaegs, filterLastAssistantMessage } from '@renderer/utils/messageUtils/filters'
|
||||
import { findFileBlocks, getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||
import {
|
||||
buildSystemPromptWithThinkTool,
|
||||
@ -66,7 +67,7 @@ import {
|
||||
import { processKnowledgeSearch } from './KnowledgeService'
|
||||
import { MemoryProcessor } from './MemoryProcessor'
|
||||
import {
|
||||
filterContextMessages,
|
||||
filterAfterContextClearMessages,
|
||||
filterEmptyMessages,
|
||||
filterUsefulMessages,
|
||||
filterUserRoleStartMessages
|
||||
@ -441,7 +442,7 @@ export async function fetchChatCompletion({
|
||||
const AI = new AiProvider(provider)
|
||||
|
||||
// Make sure that 'Clear Context' works for all scenarios including external tool and normal chat.
|
||||
messages = filterContextMessages(messages)
|
||||
const filteredMessages1 = filterAfterContextClearMessages(messages)
|
||||
|
||||
const lastUserMessage = findLast(messages, (m) => m.role === 'user')
|
||||
const lastAnswer = findLast(messages, (m) => m.role === 'assistant')
|
||||
@ -459,10 +460,14 @@ export async function fetchChatCompletion({
|
||||
|
||||
const { maxTokens, contextCount } = getAssistantSettings(assistant)
|
||||
|
||||
const filteredMessages = filterUsefulMessages(messages)
|
||||
const filteredMessages2 = filterUsefulMessages(filteredMessages1)
|
||||
|
||||
const filteredMessages3 = filterLastAssistantMessage(filteredMessages2)
|
||||
|
||||
const filteredMessages4 = filterAdjacentUserMessaegs(filteredMessages3)
|
||||
|
||||
const _messages = filterUserRoleStartMessages(
|
||||
filterEmptyMessages(filterContextMessages(takeRight(filteredMessages, contextCount + 2))) // 取原来几个provider的最大值
|
||||
filterEmptyMessages(filterAfterContextClearMessages(takeRight(filteredMessages4, contextCount + 2))) // 取原来几个provider的最大值
|
||||
)
|
||||
|
||||
// FIXME: qwen3即使关闭思考仍然会导致enableReasoning的结果为true
|
||||
|
||||
@ -21,10 +21,10 @@ import {
|
||||
createMessage,
|
||||
resetMessage
|
||||
} from '@renderer/utils/messageUtils/create'
|
||||
import { filterContextMessages } from '@renderer/utils/messageUtils/filters'
|
||||
import { getMainTextContent } from '@renderer/utils/messageUtils/find'
|
||||
import dayjs from 'dayjs'
|
||||
import { t } from 'i18next'
|
||||
import { takeRight } from 'lodash'
|
||||
import { NavigateFunction } from 'react-router'
|
||||
|
||||
import { getAssistantById, getAssistantProvider, getDefaultModel } from './AssistantService'
|
||||
@ -34,7 +34,7 @@ import FileManager from './FileManager'
|
||||
const logger = loggerService.withContext('MessagesService')
|
||||
|
||||
export {
|
||||
filterContextMessages,
|
||||
filterAfterContextClearMessages,
|
||||
filterEmptyMessages,
|
||||
filterMessages,
|
||||
filterUsefulMessages,
|
||||
@ -43,23 +43,14 @@ export {
|
||||
} from '@renderer/utils/messageUtils/filters'
|
||||
|
||||
export function getContextCount(assistant: Assistant, messages: Message[]) {
|
||||
const rawContextCount = assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT
|
||||
const maxContextCount = rawContextCount === MAX_CONTEXT_COUNT ? UNLIMITED_CONTEXT_COUNT : rawContextCount
|
||||
const settingContextCount = assistant?.settings?.contextCount ?? DEFAULT_CONTEXTCOUNT
|
||||
const actualContextCount = settingContextCount === MAX_CONTEXT_COUNT ? UNLIMITED_CONTEXT_COUNT : settingContextCount
|
||||
|
||||
const _messages = takeRight(messages, maxContextCount)
|
||||
|
||||
const clearIndex = _messages.findLastIndex((message) => message.type === 'clear')
|
||||
|
||||
let currentContextCount = 0
|
||||
if (clearIndex === -1) {
|
||||
currentContextCount = _messages.length
|
||||
} else {
|
||||
currentContextCount = _messages.length - (clearIndex + 1)
|
||||
}
|
||||
const contextMsgs = filterContextMessages(messages, actualContextCount)
|
||||
|
||||
return {
|
||||
current: currentContextCount,
|
||||
max: rawContextCount
|
||||
current: contextMsgs.length,
|
||||
max: settingContextCount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { flatten, takeRight } from 'lodash'
|
||||
import { approximateTokenSize } from 'tokenx'
|
||||
|
||||
import { getAssistantSettings } from './AssistantService'
|
||||
import { filterContextMessages, filterMessages } from './MessagesService'
|
||||
import { filterAfterContextClearMessages, filterMessages } from './MessagesService'
|
||||
|
||||
interface MessageItem {
|
||||
name?: string
|
||||
@ -167,7 +167,7 @@ export async function estimateMessagesUsage({
|
||||
export async function estimateHistoryTokens(assistant: Assistant, msgs: Message[]) {
|
||||
const { contextCount } = getAssistantSettings(assistant)
|
||||
const maxContextCount = contextCount
|
||||
const messages = filterMessages(filterContextMessages(takeRight(msgs, maxContextCount)))
|
||||
const messages = filterMessages(filterAfterContextClearMessages(takeRight(msgs, maxContextCount)))
|
||||
|
||||
// 有 usage 数据的消息,快速计算总数
|
||||
const uasageTokens = messages
|
||||
|
||||
@ -4,11 +4,13 @@ import type { Message } from '@renderer/types/newMessage' // Assuming correct Me
|
||||
import { MessageBlockType } from '@renderer/types/newMessage'
|
||||
// May need Block types if refactoring to use them
|
||||
// import type { MessageBlock, MainTextMessageBlock } from '@renderer/types/newMessageTypes';
|
||||
import { remove } from 'lodash'
|
||||
import { remove, takeRight } from 'lodash'
|
||||
import { isEmpty } from 'lodash'
|
||||
// Assuming getGroupedMessages is also moved here or imported
|
||||
// import { getGroupedMessages } from './path/to/getGroupedMessages';
|
||||
|
||||
// const logger = loggerService.withContext('Utils.filter')
|
||||
|
||||
/**
|
||||
* Filters out messages of type '@' or 'clear' and messages without main text content.
|
||||
*/
|
||||
@ -27,7 +29,7 @@ export const filterMessages = (messages: Message[]) => {
|
||||
/**
|
||||
* Filters messages to include only those after the last 'clear' type message.
|
||||
*/
|
||||
export function filterContextMessages(messages: Message[]): Message[] {
|
||||
export function filterAfterContextClearMessages(messages: Message[]): Message[] {
|
||||
const clearIndex = messages.findLastIndex((message) => message.type === 'clear')
|
||||
|
||||
if (clearIndex === -1) {
|
||||
@ -95,17 +97,16 @@ export function getGroupedMessages(messages: Message[]): { [key: string]: (Messa
|
||||
groups[key] = []
|
||||
}
|
||||
groups[key].push({ ...message, index }) // Add message with its original index
|
||||
// Sort by index within group to maintain original order
|
||||
groups[key].sort((a, b) => b.index - a.index)
|
||||
})
|
||||
return groups
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters messages based on the 'useful' flag and message role sequences.
|
||||
* Only remain one message in a group. Either useful or fallback to the last message in the group.
|
||||
*/
|
||||
export function filterUsefulMessages(messages: Message[]): Message[] {
|
||||
let _messages = [...messages]
|
||||
const _messages = [...messages]
|
||||
const groupedMessages = getGroupedMessages(messages)
|
||||
|
||||
Object.entries(groupedMessages).forEach(([key, groupedMsgs]) => {
|
||||
@ -119,8 +120,8 @@ export function filterUsefulMessages(messages: Message[]): Message[] {
|
||||
}
|
||||
})
|
||||
} else if (groupedMsgs.length > 0) {
|
||||
// Keep only the last message if none are marked useful
|
||||
const messagesToRemove = groupedMsgs.slice(0, -1)
|
||||
// Keep only the first message if none are marked useful
|
||||
const messagesToRemove = groupedMsgs.slice(1)
|
||||
messagesToRemove.forEach((m) => {
|
||||
remove(_messages, (o) => o.id === m.id)
|
||||
})
|
||||
@ -128,17 +129,23 @@ export function filterUsefulMessages(messages: Message[]): Message[] {
|
||||
}
|
||||
})
|
||||
|
||||
return _messages
|
||||
}
|
||||
|
||||
export function filterLastAssistantMessage(messages: Message[]): Message[] {
|
||||
const _messages = [...messages]
|
||||
// Remove trailing assistant messages
|
||||
while (_messages.length > 0 && _messages[_messages.length - 1].role === 'assistant') {
|
||||
_messages.pop()
|
||||
}
|
||||
return _messages
|
||||
}
|
||||
|
||||
export function filterAdjacentUserMessaegs(messages: Message[]): Message[] {
|
||||
// Filter adjacent user messages, keeping only the last one
|
||||
_messages = _messages.filter((message, index, origin) => {
|
||||
return messages.filter((message, index, origin) => {
|
||||
return !(message.role === 'user' && index + 1 < origin.length && origin[index + 1].role === 'user')
|
||||
})
|
||||
|
||||
return _messages
|
||||
}
|
||||
|
||||
// Note: getGroupedMessages might also need to be moved or imported.
|
||||
@ -154,3 +161,27 @@ export function filterUsefulMessages(messages: Message[]): Message[] {
|
||||
// })
|
||||
// return groups
|
||||
// }
|
||||
|
||||
/**
|
||||
* Filters and processes messages based on context requirements
|
||||
* @param messages - Array of messages to be filtered
|
||||
* @param contextCount - Number of messages to keep in context (excluding new user and assistant messages)
|
||||
* @returns Filtered array of messages that:
|
||||
* 1. Only includes messages after the last context clear
|
||||
* 2. Only includes useful message in a group (based on useful flag)
|
||||
* 3. Limited to contextCount + 2 messages (including space for new user/assistant messages)
|
||||
* 4. Starts from first user message
|
||||
* 5. Excludes empty messages
|
||||
*/
|
||||
export function filterContextMessages(messages: Message[], contextCount: number): Message[] {
|
||||
// NOTE: 和 fetchCompletions 中过滤消息的逻辑相同。
|
||||
// 按理说 fetchCompletions 也可以复用这个函数,不过 fetchCompletions 不敢随便乱改,后面再考虑重构吧
|
||||
const afterContextClearMsgs = filterAfterContextClearMessages(messages)
|
||||
const usefulMsgs = filterUsefulMessages(afterContextClearMsgs)
|
||||
const adjacentRemovedMsgs = filterAdjacentUserMessaegs(usefulMsgs)
|
||||
const filteredMessages = filterUserRoleStartMessages(
|
||||
filterEmptyMessages(takeRight(adjacentRemovedMsgs, contextCount))
|
||||
)
|
||||
|
||||
return filteredMessages
|
||||
}
|
||||
|
||||
@ -203,6 +203,7 @@ export const findTranslationBlocks = (message: Message): TranslationMessageBlock
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* 构造带工具调用结果的消息内容
|
||||
* @param blocks
|
||||
* @returns
|
||||
|
||||
Loading…
Reference in New Issue
Block a user