refact: 多模型回答优化

This commit is contained in:
Teo 2025-01-20 16:53:38 +08:00 committed by 亢奋猫
parent 28c18b6651
commit e72e324155
15 changed files with 205 additions and 52 deletions

View File

@ -86,6 +86,7 @@
"message.new.branch.created": "New Branch Created",
"message.regenerate.model": "Switch Model",
"message.new.context": "New Context",
"message.useful": "Helpful",
"save": "Save",
"settings.code_collapsible": "Code block collapsible",
"settings.context_count": "Context",

View File

@ -86,6 +86,7 @@
"message.new.branch.created": "新しいブランチが作成されました",
"message.regenerate.model": "モデルを切り替え",
"message.new.context": "新しいコンテキスト",
"message.useful": "役立つ",
"save": "保存",
"settings.code_collapsible": "コードブロックを折りたたむ",
"settings.context_count": "コンテキスト",

View File

@ -86,6 +86,7 @@
"message.new.branch.created": "Новая ветка создана",
"message.regenerate.model": "Переключить модель",
"message.new.context": "Новый контекст",
"message.useful": "Полезно",
"save": "Сохранить",
"settings.code_collapsible": "Блок кода свернут",
"settings.context_count": "Контекст",

View File

@ -86,6 +86,7 @@
"message.new.branch.created": "新分支已创建",
"message.regenerate.model": "切换模型",
"message.new.context": "清除上下文",
"message.useful": "有用",
"save": "保存",
"settings.code_collapsible": "代码块可折叠",
"settings.context_count": "上下文数",
@ -253,6 +254,10 @@
"message.style": "消息样式",
"message.style.bubble": "气泡",
"message.style.plain": "简洁",
"message.multi_model_style": "多模型回答样式",
"message.multi_model_style.horizontal": "水平",
"message.multi_model_style.vertical": "垂直",
"message.multi_model_style.fold": "折叠",
"reset.confirm.content": "确定要重置所有数据吗?",
"reset.double.confirm.content": "你的全部数据都会丢失,如果没有备份数据,将无法恢复,确定要继续吗?",
"reset.double.confirm.title": "数据丢失!!!",

View File

@ -86,6 +86,7 @@
"message.new.branch.created": "新分支已建立",
"message.regenerate.model": "切換模型",
"message.new.context": "新上下文",
"message.useful": "有用",
"save": "保存",
"settings.code_collapsible": "代码块可折叠",
"settings.context_count": "上下文",

View File

@ -139,7 +139,6 @@ const Inputbar: FC<Props> = ({ assistant: _assistant, setActiveTopic }) => {
setText('')
setFiles([])
setMentionModels([])
setTimeout(() => setText(''), 500)
setTimeout(() => resizeTextArea(), 0)

View File

@ -194,7 +194,6 @@ const MessageItem: FC<Props> = ({
const MessageContainer = styled.div`
display: flex;
flex-direction: column;
padding: 15px 20px 0 20px;
position: relative;
transition: background-color 0.3s ease;
&.message-highlight {

View File

@ -0,0 +1,88 @@
import { useSettings } from '@renderer/hooks/useSettings'
import { Message, Topic } from '@renderer/types'
import { Segmented } from 'antd'
import { Dispatch, FC, SetStateAction, useState } from 'react'
import styled from 'styled-components'
import MessageItem from './Message'
interface Props {
messages: (Message & { index: number })[]
topic?: Topic
hidePresetMessages?: boolean
onGetMessages?: () => Message[]
onSetMessages?: Dispatch<SetStateAction<Message[]>>
onDeleteMessage?: (message: Message) => void
}
const MessageGroup: FC<Props> = ({
messages,
topic,
hidePresetMessages,
onDeleteMessage,
onSetMessages,
onGetMessages
}) => {
const { multiModelMessageStyle } = useSettings()
const messageLength = messages.length
const [selectedIndex, setSelectedIndex] = useState(0)
return (
<GroupContainer>
{messageLength > 1 && multiModelMessageStyle === 'fold' && (
<Segmented
value={selectedIndex.toString()}
onChange={(value) => setSelectedIndex(Number(value))}
options={messages.map((message, index) => ({
label: `@${message.modelId}`,
value: index.toString()
}))}
size="small"
/>
)}
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}>
{messages.map((message, index) => (
<MessageWrapper $layout={multiModelMessageStyle} $selected={index === selectedIndex} key={message.id}>
<MessageItem
message={message}
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
))}
</GridContainer>
</GroupContainer>
)
}
const GroupContainer = styled.div``
const GridContainer = styled.div<{ $count: number; $layout: 'fold' | 'horizontal' | 'vertical' }>`
width: 100%;
overflow-x: auto;
display: grid;
grid-template-columns: repeat(
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
minmax(400px, 1fr)
);
gap: 16px;
`
const MessageWrapper = styled.div<{ $layout: 'fold' | 'horizontal' | 'vertical'; $selected: boolean }>`
width: 100%;
display: ${(props) => {
if (props.$layout === 'fold') {
return props.$selected ? 'block' : 'none'
}
if (props.$layout === 'horizontal') {
return 'inline-block'
}
return 'block'
}};
`
export default MessageGroup

View File

@ -3,6 +3,8 @@ import {
DeleteOutlined,
EditOutlined,
ForkOutlined,
LikeFilled,
LikeOutlined,
MenuOutlined,
QuestionCircleOutlined,
SaveOutlined,
@ -43,7 +45,6 @@ const MessageMenubar: FC<Props> = (props) => {
isLastMessage,
isAssistantMessage,
assistantModel,
setModel,
onEditMessage,
onDeleteMessage,
onGetMessages
@ -53,7 +54,6 @@ const MessageMenubar: FC<Props> = (props) => {
const [isTranslating, setIsTranslating] = useState(false)
const isUserMessage = message.role === 'user'
const canRegenerate = isLastMessage && isAssistantMessage
const onCopy = useCallback(() => {
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
@ -62,14 +62,6 @@ const MessageMenubar: FC<Props> = (props) => {
setTimeout(() => setCopied(false), 2000)
}, [message.content, t])
const onRegenerate = useCallback(
(model: Model) => {
setModel(model)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100)
},
[setModel]
)
const onNewBranch = useCallback(async () => {
await modelGenerating()
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
@ -175,25 +167,27 @@ const MessageMenubar: FC<Props> = (props) => {
[message, onEdit, onNewBranch, t]
)
const onAtModelRegenerate = async () => {
await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model })
selectedModel && onRegenerate(selectedModel)
}
const onDeleteAndRegenerate = async () => {
await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model })
if (!selectedModel) return
onEditMessage?.({
...message,
content: '',
reasoning_content: undefined,
metrics: undefined,
status: 'sending',
modelId: assistantModel?.id || model?.id,
modelId: selectedModel.id || assistantModel?.id || model?.id,
model: selectedModel,
translatedContent: undefined
})
}
const onUseful = useCallback(() => {
onEditMessage?.({ ...message, useful: !message.useful })
}, [message, onEditMessage])
return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && (
@ -210,23 +204,9 @@ const MessageMenubar: FC<Props> = (props) => {
</ActionButton>
</Tooltip>
{isAssistantMessage && (
<Popconfirm
title={t('message.regenerate.confirm')}
okButtonProps={{ danger: true }}
destroyTooltipOnHide
icon={<QuestionCircleOutlined style={{ color: 'red' }} />}
onConfirm={onDeleteAndRegenerate}>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button">
<SyncOutlined />
</ActionButton>
</Tooltip>
</Popconfirm>
)}
{canRegenerate && (
<Tooltip title={t('chat.message.regenerate.model')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onAtModelRegenerate}>
<i className="iconfont icon-at"></i>
<Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onDeleteAndRegenerate}>
<SyncOutlined />
</ActionButton>
</Tooltip>
)}
@ -281,6 +261,14 @@ const MessageMenubar: FC<Props> = (props) => {
</Tooltip>
</Dropdown>
)}
{isAssistantMessage && (
<Tooltip title={t('chat.message.useful')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button" onClick={onUseful}>
{message.useful ? <LikeFilled /> : <LikeOutlined />}
</ActionButton>
</Tooltip>
)}
<Popconfirm
title={t('message.message.delete.content')}
okButtonProps={{ danger: true }}

View File

@ -12,6 +12,7 @@ import {
filterMessages,
getAssistantMessage,
getContextCount,
getGroupedMessages,
getUserMessage
} from '@renderer/services/MessagesService'
import { estimateHistoryTokens } from '@renderer/services/TokenService'
@ -25,7 +26,7 @@ import BeatLoader from 'react-spinners/BeatLoader'
import styled from 'styled-components'
import Suggestions from '../components/Suggestions'
import MessageItem from './Message'
import MessageGroup from './MessageGroup'
import NarrowLayout from './NarrowLayout'
import Prompt from './Prompt'
@ -53,10 +54,12 @@ const LoaderContainer = styled.div<LoaderProps>`
const ScrollContainer = styled.div`
display: flex;
flex-direction: column-reverse;
padding: 16px 16px;
gap: 32px;
`
interface ContainerProps {
right?: boolean
$right?: boolean
}
const Container = styled(Scrollbar)<ContainerProps>`
@ -79,6 +82,8 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
const { updateTopic, addTopic } = useAssistant(assistant.id)
const { showTopics, topicPosition, showAssistants, enableTopicNaming } = useSettings()
const groupedMessages = getGroupedMessages(displayMessages)
const INITIAL_MESSAGES_COUNT = 20
const LOAD_MORE_COUNT = 20
@ -102,10 +107,13 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
message.mentions.forEach((m) => {
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
assistantMessage.model = m
assistantMessage.askId = message.id
assistantMessages.push(assistantMessage)
})
} else {
assistantMessages.push(getAssistantMessage({ assistant, topic }))
const assistantMessage = getAssistantMessage({ assistant, topic })
assistantMessage.askId = message.id
assistantMessages.push(assistantMessage)
}
setMessages((prev) => {
@ -214,7 +222,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
setActiveTopic(newTopic)
autoRenameTopic()
// 由于复制了消<EFBFBD><EFBFBD><EFBFBD>,消息中附带的文件的总数变了,需要更新
// 由于复制了消,消息中附带的文件的总数变了,需要更新
const filesArr = branchMessages.map((m) => m.files)
const files = flatten(filesArr).filter(Boolean)
files.map(async (f) => {
@ -293,7 +301,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
style={{ maxWidth }}
key={assistant.id}
ref={containerRef}
right={topicPosition === 'left'}>
$right={topicPosition === 'left'}>
<NarrowLayout style={{ display: 'flex', flexDirection: 'column-reverse' }}>
<Suggestions assistant={assistant} messages={messages} />
<InfiniteScroll
@ -307,12 +315,11 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
<LoaderContainer $loading={isLoadingMore}>
<BeatLoader size={8} color="var(--color-text-2)" />
</LoaderContainer>
{displayMessages.map((message, index) => (
<MessageItem
key={message.id}
message={message}
{Object.entries(groupedMessages).map(([key, messages]) => (
<MessageGroup
key={key}
messages={messages}
topic={topic}
index={index}
hidePresetMessages={assistant.settings?.hideMessages}
onSetMessages={setMessages}
onDeleteMessage={onDeleteMessage}

View File

@ -22,6 +22,7 @@ import {
setMathEngine,
setMessageFont,
setMessageStyle,
setMultiModelMessageStyle,
setPasteLongTextAsFile,
setPasteLongTextThreshold,
setRenderInputMessageAsMarkdown,
@ -64,7 +65,8 @@ const SettingsTab: FC<Props> = (props) => {
codeCollapsible,
mathEngine,
autoTranslateWithSpace,
pasteLongTextThreshold
pasteLongTextThreshold,
multiModelMessageStyle
} = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
@ -255,6 +257,19 @@ const SettingsTab: FC<Props> = (props) => {
</Select>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.multi_model_style')}</SettingRowTitleSmall>
<Select
size="small"
value={multiModelMessageStyle}
onChange={(value) => dispatch(setMultiModelMessageStyle(value))}
style={{ width: 135 }}>
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option>
<Select.Option value="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
</Select>
</SettingRow>
<SettingDivider />
<SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<Select

View File

@ -13,7 +13,7 @@ import {
getTranslateModel
} from './AssistantService'
import { EVENT_NAMES, EventEmitter } from './EventService'
import { filterMessages } from './MessagesService'
import { filterMessages, filterUsefulMessages } from './MessagesService'
import { estimateMessagesUsage } from './TokenService'
export async function fetchChatCompletion({
@ -53,7 +53,7 @@ export async function fetchChatCompletion({
let _messages: Message[] = []
await AI.completions({
messages,
messages: filterUsefulMessages(messages),
assistant,
onFilterMessages: (messages) => (_messages = messages),
onChunk: ({ text, reasoning_content, usage, metrics, search }) => {

View File

@ -5,7 +5,7 @@ import i18n from '@renderer/i18n'
import store from '@renderer/store'
import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils'
import { isEmpty, takeRight } from 'lodash'
import { isEmpty, remove, takeRight } from 'lodash'
import { NavigateFunction } from 'react-router'
import { getAssistantById, getDefaultModel } from './AssistantService'
@ -109,3 +109,43 @@ export function getAssistantMessage({ assistant, topic }: { assistant: Assistant
status: 'sending'
}
}
export function filterUsefulMessages(messages: Message[]): Message[] {
const _messages = messages
const groupedMessages = getGroupedMessages(messages)
Object.entries(groupedMessages).forEach(([key, messages]) => {
if (key.startsWith('assistant')) {
const usefulMessage = messages.find((m) => m.useful === true)
if (usefulMessage) {
messages.forEach((m) => {
if (m.id !== usefulMessage.id) {
remove(_messages, (o) => o.id === m.id)
}
})
} else {
messages?.slice(0, -1).forEach((m) => {
remove(_messages, (o) => o.id === m.id)
})
}
}
})
while (_messages.length > 0 && _messages[_messages.length - 1].role === 'assistant') {
_messages.pop()
}
return _messages
}
export function getGroupedMessages(messages: Message[]): { [key: string]: (Message & { index: number })[] } {
const groups: { [key: string]: (Message & { index: number })[] } = {}
messages.forEach((message, index) => {
const key = message.askId ? 'assistant' + message.askId : 'user' + message.id
if (key && !groups[key]) {
groups[key] = []
}
groups[key].unshift({ ...message, index })
})
return groups
}

View File

@ -63,6 +63,7 @@ export interface SettingsState {
narrowMode: boolean
enableQuickAssistant: boolean
clickTrayToShowQuickAssistant: boolean
multiModelMessageStyle: 'horizontal' | 'vertical' | 'fold'
}
const initialState: SettingsState = {
@ -109,7 +110,8 @@ const initialState: SettingsState = {
},
narrowMode: false,
enableQuickAssistant: false,
clickTrayToShowQuickAssistant: false
clickTrayToShowQuickAssistant: false,
multiModelMessageStyle: 'vertical'
}
const settingsSlice = createSlice({
@ -251,6 +253,9 @@ const settingsSlice = createSlice({
},
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
state.enableQuickAssistant = action.payload
},
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => {
state.multiModelMessageStyle = action.payload
}
}
})
@ -298,7 +303,8 @@ export const {
setSidebarIcons,
setNarrowMode,
setClickTrayToShowQuickAssistant,
setEnableQuickAssistant
setEnableQuickAssistant,
setMultiModelMessageStyle
} = settingsSlice.actions
export default settingsSlice.reducer

View File

@ -66,6 +66,8 @@ export type Message = {
// Gemini
groundingMetadata?: any
}
askId?: string
useful?: boolean
}
export type Metrics = {