feat: enhance message grouping and styling

- Added new styles for message thought containers and group message wrappers to improve UI layout.
- Updated MessageGroup component to dynamically set the selected message index based on message length.
- Introduced a new event for appending messages, enhancing message handling capabilities.
- Refactored MessageMenubar to support the new append message functionality.
- Adjusted multi-model message style setting to 'fold' for better user experience.
- Improved responsiveness of message grid layout for smaller screens.
This commit is contained in:
kangfenmao 2025-01-22 12:03:08 +08:00
parent 462ac39897
commit 00d91ecf01
10 changed files with 77 additions and 25 deletions

View File

@ -234,6 +234,9 @@ body,
border-radius: 8px; border-radius: 8px;
padding: 10px 15px 0 15px; padding: 10px 15px 0 15px;
} }
.message-thought-container {
margin-top: 8px;
}
.message-user { .message-user {
color: var(--chat-text-user); color: var(--chat-text-user);
.markdown, .markdown,
@ -246,6 +249,16 @@ body,
background-color: var(--color-white-soft); background-color: var(--color-white-soft);
} }
} }
.group-message-wrapper {
background-color: var(--color-background);
.message-content-container {
width: 100%;
border: 1px solid var(--color-background-mute);
}
}
.group-menu-bar {
background-color: var(--color-background);
}
code { code {
color: var(--color-text); color: var(--color-text);
} }

View File

@ -7,7 +7,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings' import { MultiModelMessageStyle } from '@renderer/store/settings'
import { Message, Model, Topic } from '@renderer/types' import { Message, Model, Topic } from '@renderer/types'
import { Button, Segmented } from 'antd' import { Button, Segmented } from 'antd'
import { Dispatch, FC, SetStateAction, useState } from 'react' import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
import styled, { css } from 'styled-components' import styled, { css } from 'styled-components'
import MessageItem from './Message' import MessageItem from './Message'
@ -37,7 +37,7 @@ const MessageGroup: FC<Props> = ({
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting) useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
const messageLength = messages.length const messageLength = messages.length
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
const isGrouped = messageLength > 1 const isGrouped = messageLength > 1
@ -46,6 +46,12 @@ const MessageGroup: FC<Props> = ({
askId && onDeleteGroupMessages?.(askId) askId && onDeleteGroupMessages?.(askId)
} }
useEffect(() => {
setSelectedIndex(messageLength - 1)
}, [messageLength])
const isHorizontal = multiModelMessageStyle === 'horizontal'
return ( return (
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}> <GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}> <GridContainer $count={messageLength} $layout={multiModelMessageStyle}>
@ -54,7 +60,8 @@ const MessageGroup: FC<Props> = ({
$layout={multiModelMessageStyle} $layout={multiModelMessageStyle}
$selected={index === selectedIndex} $selected={index === selectedIndex}
$isGrouped={isGrouped} $isGrouped={isGrouped}
key={message.id}> key={message.id}
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
<MessageItem <MessageItem
message={message} message={message}
topic={topic} topic={topic}
@ -69,8 +76,8 @@ const MessageGroup: FC<Props> = ({
))} ))}
</GridContainer> </GridContainer>
{isGrouped && ( {isGrouped && (
<GroupHeader> <GroupMenuBar className="group-menu-bar">
<HStack style={{ alignItems: 'center' }}> <HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
<LayoutContainer> <LayoutContainer>
{['fold', 'horizontal', 'vertical'].map((layout) => ( {['fold', 'horizontal', 'vertical'].map((layout) => (
<LayoutOption <LayoutOption
@ -115,7 +122,7 @@ const MessageGroup: FC<Props> = ({
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />} icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
onClick={onDelete} onClick={onDelete}
/> />
</GroupHeader> </GroupMenuBar>
)} )}
</GroupContainer> </GroupContainer>
) )
@ -133,6 +140,12 @@ const GridContainer = styled(Scrollbar)<{ $count: number; $layout: MultiModelMes
minmax(550px, 1fr) minmax(550px, 1fr)
); );
gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')}; gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
@media (max-width: 800px) {
grid-template-columns: repeat(
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
minmax(400px, 1fr)
);
}
` `
interface MessageWrapperProps { interface MessageWrapperProps {
@ -160,31 +173,24 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
border-radius: 6px; border-radius: 6px;
max-height: 600px; max-height: 600px;
overflow-y: auto; overflow-y: auto;
margin-bottom: 10px;
` `
} }
return '' return ''
}} }}
` `
const GroupHeader = styled.div` const GroupMenuBar = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
gap: 10px; gap: 10px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
padding: 8px 10px; padding: 6px 10px;
border-radius: 6px; border-radius: 6px;
margin-top: 10px; margin-top: 10px;
justify-content: space-between; justify-content: space-between;
` overflow: hidden;
const ModelsContainer = styled(Scrollbar)`
display: flex;
flex-direction: column;
justify-content: space-between;
&::-webkit-scrollbar {
display: none;
}
` `
const LayoutContainer = styled.div` const LayoutContainer = styled.div`
@ -205,11 +211,20 @@ const LayoutOption = styled.div<{ active: boolean }>`
} }
` `
const ModelsContainer = styled(Scrollbar)`
display: flex;
flex-direction: column;
justify-content: space-between;
&::-webkit-scrollbar {
display: none;
}
`
const SegmentedLabel = styled.div` const SegmentedLabel = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
padding: 5px 0; padding: 3px 0;
` `
const ModelName = styled.span` const ModelName = styled.span`

View File

@ -173,16 +173,23 @@ const MessageMenubar: FC<Props> = (props) => {
const selectedModel = await SelectModelPopup.show({ model }) const selectedModel = await SelectModelPopup.show({ model })
if (!selectedModel) return if (!selectedModel) return
onEditMessage?.({ const _message: Message = {
...message, ...message,
content: '', content: '',
reasoning_content: undefined, reasoning_content: undefined,
metrics: undefined, metrics: undefined,
status: 'sending', status: 'sending',
modelId: selectedModel.id || assistantModel?.id || model?.id, modelId: selectedModel.id,
model: selectedModel, model: selectedModel,
translatedContent: undefined translatedContent: undefined,
}) metadata: undefined
}
if (message.askId && message.model) {
return EventEmitter.emit(EVENT_NAMES.APPEND_MESSAGE, { ..._message, id: uuid() })
}
onEditMessage?.(_message)
} }
const onUseful = useCallback(() => { const onUseful = useCallback(() => {

View File

@ -23,6 +23,7 @@ const MessageThought: FC<Props> = ({ message }) => {
return ( return (
<CollapseContainer <CollapseContainer
className="message-thought-container"
items={[ items={[
{ {
key: 'thought', key: 'thought',

View File

@ -68,6 +68,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
const onSendMessage = useCallback( const onSendMessage = useCallback(
async (message: Message) => { async (message: Message) => {
const assistantMessages: Message[] = [] const assistantMessages: Message[] = []
if (message.mentions?.length) { if (message.mentions?.length) {
message.mentions.forEach((m) => { message.mentions.forEach((m) => {
const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic }) const assistantMessage = getAssistantMessage({ assistant: { ...assistant, model: m }, topic })
@ -92,6 +93,17 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
[assistant, scrollToBottom, topic] [assistant, scrollToBottom, topic]
) )
const onAppendMessage = useCallback(
(message: Message) => {
setMessages((prev) => {
const messages = prev.concat([message])
db.topics.put({ id: topic.id, messages })
return messages
})
},
[topic.id]
)
const autoRenameTopic = useCallback(async () => { const autoRenameTopic = useCallback(async () => {
const _topic = getTopic(assistant, topic.id) const _topic = getTopic(assistant, topic.id)
@ -146,6 +158,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
useEffect(() => { useEffect(() => {
const unsubscribes = [ const unsubscribes = [
EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage), EventEmitter.on(EVENT_NAMES.SEND_MESSAGE, onSendMessage),
EventEmitter.on(EVENT_NAMES.APPEND_MESSAGE, onAppendMessage),
EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async () => { EventEmitter.on(EVENT_NAMES.RECEIVE_MESSAGE, async () => {
setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100) setTimeout(() => EventEmitter.emit(EVENT_NAMES.AI_AUTO_RENAME), 100)
}), }),
@ -215,6 +228,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
assistant, assistant,
autoRenameTopic, autoRenameTopic,
messages, messages,
onAppendMessage,
onDeleteMessage, onDeleteMessage,
onSendMessage, onSendMessage,
scrollToBottom, scrollToBottom,

View File

@ -264,9 +264,9 @@ const SettingsTab: FC<Props> = (props) => {
value={multiModelMessageStyle} value={multiModelMessageStyle}
onChange={(value) => dispatch(setMultiModelMessageStyle(value))} onChange={(value) => dispatch(setMultiModelMessageStyle(value))}
style={{ width: 135 }}> style={{ width: 135 }}>
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
<Select.Option value="horizontal">{t('message.message.multi_model_style.horizontal')}</Select.Option> <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="vertical">{t('message.message.multi_model_style.vertical')}</Select.Option>
<Select.Option value="fold">{t('message.message.multi_model_style.fold')}</Select.Option>
</Select> </Select>
</SettingRow> </SettingRow>
<SettingDivider /> <SettingDivider />

View File

@ -4,6 +4,7 @@ export const EventEmitter = new Emittery()
export const EVENT_NAMES = { export const EVENT_NAMES = {
SEND_MESSAGE: 'SEND_MESSAGE', SEND_MESSAGE: 'SEND_MESSAGE',
APPEND_MESSAGE: 'APPEND_MESSAGE',
RECEIVE_MESSAGE: 'RECEIVE_MESSAGE', RECEIVE_MESSAGE: 'RECEIVE_MESSAGE',
AI_AUTO_RENAME: 'AI_AUTO_RENAME', AI_AUTO_RENAME: 'AI_AUTO_RENAME',
CLEAR_MESSAGES: 'CLEAR_MESSAGES', CLEAR_MESSAGES: 'CLEAR_MESSAGES',

View File

@ -103,6 +103,7 @@ export function getAssistantMessage({ assistant, topic }: { assistant: Assistant
content: '', content: '',
assistantId: assistant.id, assistantId: assistant.id,
topicId: topic.id, topicId: topic.id,
model,
modelId: model.id, modelId: model.id,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
type: 'text', type: 'text',

View File

@ -879,7 +879,7 @@ const migrateConfig = {
return state return state
}, },
'60': (state: RootState) => { '60': (state: RootState) => {
state.settings.multiModelMessageStyle = 'vertical' state.settings.multiModelMessageStyle = 'fold'
return state return state
} }
} }

View File

@ -113,7 +113,7 @@ const initialState: SettingsState = {
narrowMode: false, narrowMode: false,
enableQuickAssistant: false, enableQuickAssistant: false,
clickTrayToShowQuickAssistant: false, clickTrayToShowQuickAssistant: false,
multiModelMessageStyle: 'horizontal' multiModelMessageStyle: 'fold'
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({