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.new.branch.created": "New Branch Created",
"message.regenerate.model": "Switch Model", "message.regenerate.model": "Switch Model",
"message.new.context": "New Context", "message.new.context": "New Context",
"message.useful": "Helpful",
"save": "Save", "save": "Save",
"settings.code_collapsible": "Code block collapsible", "settings.code_collapsible": "Code block collapsible",
"settings.context_count": "Context", "settings.context_count": "Context",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -194,7 +194,6 @@ const MessageItem: FC<Props> = ({
const MessageContainer = styled.div` const MessageContainer = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 15px 20px 0 20px;
position: relative; position: relative;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
&.message-highlight { &.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, DeleteOutlined,
EditOutlined, EditOutlined,
ForkOutlined, ForkOutlined,
LikeFilled,
LikeOutlined,
MenuOutlined, MenuOutlined,
QuestionCircleOutlined, QuestionCircleOutlined,
SaveOutlined, SaveOutlined,
@ -43,7 +45,6 @@ const MessageMenubar: FC<Props> = (props) => {
isLastMessage, isLastMessage,
isAssistantMessage, isAssistantMessage,
assistantModel, assistantModel,
setModel,
onEditMessage, onEditMessage,
onDeleteMessage, onDeleteMessage,
onGetMessages onGetMessages
@ -53,7 +54,6 @@ const MessageMenubar: FC<Props> = (props) => {
const [isTranslating, setIsTranslating] = useState(false) const [isTranslating, setIsTranslating] = useState(false)
const isUserMessage = message.role === 'user' const isUserMessage = message.role === 'user'
const canRegenerate = isLastMessage && isAssistantMessage
const onCopy = useCallback(() => { const onCopy = useCallback(() => {
navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content)) navigator.clipboard.writeText(removeTrailingDoubleSpaces(message.content))
@ -62,14 +62,6 @@ const MessageMenubar: FC<Props> = (props) => {
setTimeout(() => setCopied(false), 2000) setTimeout(() => setCopied(false), 2000)
}, [message.content, t]) }, [message.content, t])
const onRegenerate = useCallback(
(model: Model) => {
setModel(model)
setTimeout(() => EventEmitter.emit(EVENT_NAMES.REGENERATE_MESSAGE, model), 100)
},
[setModel]
)
const onNewBranch = useCallback(async () => { const onNewBranch = useCallback(async () => {
await modelGenerating() await modelGenerating()
EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index) EventEmitter.emit(EVENT_NAMES.NEW_BRANCH, index)
@ -175,25 +167,27 @@ const MessageMenubar: FC<Props> = (props) => {
[message, onEdit, onNewBranch, t] [message, onEdit, onNewBranch, t]
) )
const onAtModelRegenerate = async () => {
await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model })
selectedModel && onRegenerate(selectedModel)
}
const onDeleteAndRegenerate = async () => { const onDeleteAndRegenerate = async () => {
await modelGenerating() await modelGenerating()
const selectedModel = await SelectModelPopup.show({ model })
if (!selectedModel) return
onEditMessage?.({ onEditMessage?.({
...message, ...message,
content: '', content: '',
reasoning_content: undefined, reasoning_content: undefined,
metrics: undefined, metrics: undefined,
status: 'sending', status: 'sending',
modelId: assistantModel?.id || model?.id, modelId: selectedModel.id || assistantModel?.id || model?.id,
model: selectedModel,
translatedContent: undefined translatedContent: undefined
}) })
} }
const onUseful = useCallback(() => {
onEditMessage?.({ ...message, useful: !message.useful })
}, [message, onEditMessage])
return ( return (
<MenusBar className={`menubar ${isLastMessage && 'show'}`}> <MenusBar className={`menubar ${isLastMessage && 'show'}`}>
{message.role === 'user' && ( {message.role === 'user' && (
@ -210,25 +204,11 @@ const MessageMenubar: FC<Props> = (props) => {
</ActionButton> </ActionButton>
</Tooltip> </Tooltip>
{isAssistantMessage && ( {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}> <Tooltip title={t('common.regenerate')} mouseEnterDelay={0.8}>
<ActionButton className="message-action-button"> <ActionButton className="message-action-button" onClick={onDeleteAndRegenerate}>
<SyncOutlined /> <SyncOutlined />
</ActionButton> </ActionButton>
</Tooltip> </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>
</ActionButton>
</Tooltip>
)} )}
{!isUserMessage && ( {!isUserMessage && (
<Dropdown <Dropdown
@ -281,6 +261,14 @@ const MessageMenubar: FC<Props> = (props) => {
</Tooltip> </Tooltip>
</Dropdown> </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 <Popconfirm
title={t('message.message.delete.content')} title={t('message.message.delete.content')}
okButtonProps={{ danger: true }} okButtonProps={{ danger: true }}

View File

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

View File

@ -22,6 +22,7 @@ import {
setMathEngine, setMathEngine,
setMessageFont, setMessageFont,
setMessageStyle, setMessageStyle,
setMultiModelMessageStyle,
setPasteLongTextAsFile, setPasteLongTextAsFile,
setPasteLongTextThreshold, setPasteLongTextThreshold,
setRenderInputMessageAsMarkdown, setRenderInputMessageAsMarkdown,
@ -64,7 +65,8 @@ const SettingsTab: FC<Props> = (props) => {
codeCollapsible, codeCollapsible,
mathEngine, mathEngine,
autoTranslateWithSpace, autoTranslateWithSpace,
pasteLongTextThreshold pasteLongTextThreshold,
multiModelMessageStyle
} = useSettings() } = useSettings()
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => { const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
@ -255,6 +257,19 @@ const SettingsTab: FC<Props> = (props) => {
</Select> </Select>
</SettingRow> </SettingRow>
<SettingDivider /> <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> <SettingRow>
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall> <SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
<Select <Select

View File

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

View File

@ -5,7 +5,7 @@ import i18n from '@renderer/i18n'
import store from '@renderer/store' import store from '@renderer/store'
import { Assistant, Message, Topic } from '@renderer/types' import { Assistant, Message, Topic } from '@renderer/types'
import { uuid } from '@renderer/utils' import { uuid } from '@renderer/utils'
import { isEmpty, takeRight } from 'lodash' import { isEmpty, remove, takeRight } from 'lodash'
import { NavigateFunction } from 'react-router' import { NavigateFunction } from 'react-router'
import { getAssistantById, getDefaultModel } from './AssistantService' import { getAssistantById, getDefaultModel } from './AssistantService'
@ -109,3 +109,43 @@ export function getAssistantMessage({ assistant, topic }: { assistant: Assistant
status: 'sending' 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 narrowMode: boolean
enableQuickAssistant: boolean enableQuickAssistant: boolean
clickTrayToShowQuickAssistant: boolean clickTrayToShowQuickAssistant: boolean
multiModelMessageStyle: 'horizontal' | 'vertical' | 'fold'
} }
const initialState: SettingsState = { const initialState: SettingsState = {
@ -109,7 +110,8 @@ const initialState: SettingsState = {
}, },
narrowMode: false, narrowMode: false,
enableQuickAssistant: false, enableQuickAssistant: false,
clickTrayToShowQuickAssistant: false clickTrayToShowQuickAssistant: false,
multiModelMessageStyle: 'vertical'
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({
@ -251,6 +253,9 @@ const settingsSlice = createSlice({
}, },
setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => { setEnableQuickAssistant: (state, action: PayloadAction<boolean>) => {
state.enableQuickAssistant = action.payload state.enableQuickAssistant = action.payload
},
setMultiModelMessageStyle: (state, action: PayloadAction<'horizontal' | 'vertical' | 'fold'>) => {
state.multiModelMessageStyle = action.payload
} }
} }
}) })
@ -298,7 +303,8 @@ export const {
setSidebarIcons, setSidebarIcons,
setNarrowMode, setNarrowMode,
setClickTrayToShowQuickAssistant, setClickTrayToShowQuickAssistant,
setEnableQuickAssistant setEnableQuickAssistant,
setMultiModelMessageStyle
} = settingsSlice.actions } = settingsSlice.actions
export default settingsSlice.reducer export default settingsSlice.reducer

View File

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