feat: add group message action bar

This commit is contained in:
kangfenmao 2025-01-21 16:58:42 +08:00
parent ccac5358f4
commit dd464db594
7 changed files with 219 additions and 71 deletions

View File

@ -25,9 +25,10 @@ interface Props {
index?: number index?: number
total?: number total?: number
hidePresetMessages?: boolean hidePresetMessages?: boolean
style?: React.CSSProperties
onGetMessages?: () => Message[] onGetMessages?: () => Message[]
onSetMessages?: Dispatch<SetStateAction<Message[]>> onSetMessages?: Dispatch<SetStateAction<Message[]>>
onDeleteMessage?: (message: Message) => void onDeleteMessage?: (message: Message) => Promise<void>
} }
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) => const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
@ -38,6 +39,7 @@ const MessageItem: FC<Props> = ({
topic, topic,
index, index,
hidePresetMessages, hidePresetMessages,
style,
onDeleteMessage, onDeleteMessage,
onSetMessages, onSetMessages,
onGetMessages onGetMessages
@ -123,7 +125,7 @@ const MessageItem: FC<Props> = ({
onResponse: (msg) => { onResponse: (msg) => {
setMessage(msg) setMessage(msg)
if (msg.status !== 'pending') { if (msg.status !== 'pending') {
const _messages = messages.map((m) => (m.id === msg.id ? msg : m)) const _messages = onGetMessages().map((m) => (m.id === msg.id ? msg : m))
onSetMessages(_messages) onSetMessages(_messages)
db.topics.update(topic.id, { messages: _messages }) db.topics.update(topic.id, { messages: _messages })
} }
@ -157,7 +159,7 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage 'message-user': !isAssistantMessage
})} })}
ref={messageContainerRef} ref={messageContainerRef}
style={isBubbleStyle ? { alignItems: isAssistantMessage ? 'start' : 'end' } : undefined}> style={{ ...style, alignItems: isBubbleStyle ? (isAssistantMessage ? 'start' : 'end') : undefined }}>
<MessageHeader message={message} assistant={assistant} model={model} key={message.modelId} /> <MessageHeader message={message} assistant={assistant} model={model} key={message.modelId} />
<MessageContentContainer <MessageContentContainer
className="message-content-container" className="message-content-container"

View File

@ -38,7 +38,7 @@ const MessageContent: React.FC<{
return ( return (
<> <>
<Flex gap="8px" wrap> <Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={model.id}>{'@' + model.name}</MentionTag>)} {message.mentions?.map((model) => <MentionTag key={model.id}>{'@' + model.name}</MentionTag>)}
</Flex> </Flex>
<MessageThought message={message} /> <MessageThought message={message} />

View File

@ -1,8 +1,14 @@
import { ColumnHeightOutlined, ColumnWidthOutlined, DeleteOutlined, FolderOutlined } from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import { useSettings } from '@renderer/hooks/useSettings' import { useSettings } from '@renderer/hooks/useSettings'
import { Message, Topic } from '@renderer/types' import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { Segmented } from 'antd' import { MultiModelMessageStyle } from '@renderer/store/settings'
import { Message, Model, Topic } from '@renderer/types'
import { Button, Segmented } from 'antd'
import { Dispatch, FC, SetStateAction, useState } from 'react' import { Dispatch, FC, SetStateAction, useState } from 'react'
import styled from 'styled-components' import styled, { css } from 'styled-components'
import MessageItem from './Message' import MessageItem from './Message'
@ -12,7 +18,8 @@ interface Props {
hidePresetMessages?: boolean hidePresetMessages?: boolean
onGetMessages?: () => Message[] onGetMessages?: () => Message[]
onSetMessages?: Dispatch<SetStateAction<Message[]>> onSetMessages?: Dispatch<SetStateAction<Message[]>>
onDeleteMessage?: (message: Message) => void onDeleteMessage?: (message: Message) => Promise<void>
onDeleteGroupMessages?: (askId: string) => Promise<void>
} }
const MessageGroup: FC<Props> = ({ const MessageGroup: FC<Props> = ({
@ -21,33 +28,39 @@ const MessageGroup: FC<Props> = ({
hidePresetMessages, hidePresetMessages,
onDeleteMessage, onDeleteMessage,
onSetMessages, onSetMessages,
onGetMessages onGetMessages,
onDeleteGroupMessages
}) => { }) => {
const { multiModelMessageStyle } = useSettings() const { multiModelMessageStyle: multiModelMessageStyleSetting } = useSettings()
const [multiModelMessageStyle, setMultiModelMessageStyle] =
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
const messageLength = messages.length const messageLength = messages.length
const [selectedIndex, setSelectedIndex] = useState(0) const [selectedIndex, setSelectedIndex] = useState(0)
const isGrouped = messageLength > 1
const onDelete = async () => {
const askId = messages[0].askId
askId && onDeleteGroupMessages?.(askId)
}
return ( return (
<GroupContainer> <GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
{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}> <GridContainer $count={messageLength} $layout={multiModelMessageStyle}>
{messages.map((message, index) => ( {messages.map((message, index) => (
<MessageWrapper $layout={multiModelMessageStyle} $selected={index === selectedIndex} key={message.id}> <MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
key={message.id}>
<MessageItem <MessageItem
message={message} message={message}
topic={topic} topic={topic}
index={message.index} index={message.index}
hidePresetMessages={hidePresetMessages} hidePresetMessages={hidePresetMessages}
style={{ paddingTop: isGrouped && multiModelMessageStyle === 'horizontal' ? 0 : 15 }}
onSetMessages={onSetMessages} onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage} onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages} onGetMessages={onGetMessages}
@ -55,24 +68,80 @@ const MessageGroup: FC<Props> = ({
</MessageWrapper> </MessageWrapper>
))} ))}
</GridContainer> </GridContainer>
{isGrouped && (
<GroupHeader>
<HStack style={{ alignItems: 'center' }}>
<LayoutContainer>
{['fold', 'horizontal', 'vertical'].map((layout) => (
<LayoutOption
key={layout}
active={multiModelMessageStyle === layout}
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}>
{layout === 'fold' ? (
<FolderOutlined />
) : layout === 'horizontal' ? (
<ColumnWidthOutlined />
) : (
<ColumnHeightOutlined />
)}
</LayoutOption>
))}
</LayoutContainer>
{multiModelMessageStyle === 'fold' && (
<ModelsContainer>
<Segmented
value={selectedIndex.toString()}
onChange={(value) => {
setSelectedIndex(Number(value))
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
}}
options={messages.map((message, index) => ({
label: (
<SegmentedLabel>
<ModelAvatar model={message.model as Model} size={20} />
<ModelName>{message.model?.name}</ModelName>
</SegmentedLabel>
),
value: index.toString()
}))}
size="small"
/>
</ModelsContainer>
)}
</HStack>
<Button
type="text"
size="small"
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
onClick={onDelete}
/>
</GroupHeader>
)}
</GroupContainer> </GroupContainer>
) )
} }
const GroupContainer = styled.div`` const GroupContainer = styled.div<{ $isGrouped: boolean; $layout: MultiModelMessageStyle }>`
padding-top: ${({ $isGrouped, $layout }) => ($isGrouped && $layout === 'horizontal' ? '15px' : '0')};
`
const GridContainer = styled.div<{ $count: number; $layout: 'fold' | 'horizontal' | 'vertical' }>` const GridContainer = styled(Scrollbar)<{ $count: number; $layout: MultiModelMessageStyle }>`
width: 100%; width: 100%;
overflow-x: auto;
display: grid; display: grid;
grid-template-columns: repeat( grid-template-columns: repeat(
${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)}, ${(props) => (['fold', 'vertical'].includes(props.$layout) ? 1 : props.$count)},
minmax(400px, 1fr) minmax(550px, 1fr)
); );
gap: 16px; gap: ${({ $layout }) => ($layout === 'horizontal' ? '16px' : '0')};
` `
const MessageWrapper = styled.div<{ $layout: 'fold' | 'horizontal' | 'vertical'; $selected: boolean }>` interface MessageWrapperProps {
$layout: 'fold' | 'horizontal' | 'vertical'
$selected: boolean
$isGrouped: boolean
}
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
width: 100%; width: 100%;
display: ${(props) => { display: ${(props) => {
if (props.$layout === 'fold') { if (props.$layout === 'fold') {
@ -83,6 +152,69 @@ const MessageWrapper = styled.div<{ $layout: 'fold' | 'horizontal' | 'vertical';
} }
return 'block' return 'block'
}}; }};
${({ $layout, $isGrouped }) => {
if ($layout === 'horizontal' && $isGrouped) {
return css`
border: 0.5px solid var(--color-border);
padding: 10px;
border-radius: 6px;
max-height: 600px;
overflow-y: auto;
`
}
return ''
}}
`
const GroupHeader = styled.div`
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
background-color: var(--color-background-soft);
padding: 8px 10px;
border-radius: 6px;
margin-top: 10px;
justify-content: space-between;
`
const ModelsContainer = styled(Scrollbar)`
display: flex;
flex-direction: column;
justify-content: space-between;
&::-webkit-scrollbar {
display: none;
}
`
const LayoutContainer = styled.div`
display: flex;
gap: 10px;
flex-direction: row;
`
const LayoutOption = styled.div<{ active: boolean }>`
cursor: pointer;
padding: 2px 10px;
border-radius: 4px;
background-color: ${({ active }) => (active ? 'var(--color-primary)' : 'transparent')};
color: ${({ active }) => (active ? 'var(--color-white)' : 'inherit')};
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-primary)' : 'var(--color-hover)')};
}
`
const SegmentedLabel = styled.div`
display: flex;
align-items: center;
gap: 5px;
padding: 5px 0;
`
const ModelName = styled.span`
font-weight: 500;
font-size: 12px;
` `
export default MessageGroup export default MessageGroup

View File

@ -33,7 +33,7 @@ interface Props {
isAssistantMessage: boolean isAssistantMessage: boolean
setModel: (model: Model) => void setModel: (model: Model) => void
onEditMessage?: (message: Message) => void onEditMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => void onDeleteMessage?: (message: Message) => Promise<void>
onGetMessages?: () => Message[] onGetMessages?: () => Message[]
} }

View File

@ -36,41 +36,6 @@ interface Props {
setActiveTopic: (topic: Topic) => void setActiveTopic: (topic: Topic) => void
} }
interface LoaderProps {
$loading: boolean
}
const LoaderContainer = styled.div<LoaderProps>`
display: flex;
justify-content: center;
padding: 10px;
width: 100%;
background: var(--color-background);
opacity: ${(props) => (props.$loading ? 1 : 0)};
transition: opacity 0.3s ease;
pointer-events: none;
`
const ScrollContainer = styled.div`
display: flex;
flex-direction: column-reverse;
padding: 16px 16px;
gap: 32px;
`
interface ContainerProps {
$right?: boolean
}
const Container = styled(Scrollbar)<ContainerProps>`
display: flex;
flex-direction: column-reverse;
padding: 10px 0;
padding-bottom: 20px;
overflow-x: hidden;
background-color: var(--color-background);
`
const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => { const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
const [messages, setMessages] = useState<Message[]>([]) const [messages, setMessages] = useState<Message[]>([])
const [displayMessages, setDisplayMessages] = useState<Message[]>([]) const [displayMessages, setDisplayMessages] = useState<Message[]>([])
@ -151,12 +116,25 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
}, [assistant, enableTopicNaming, messages, setActiveTopic, topic.id, updateTopic]) }, [assistant, enableTopicNaming, messages, setActiveTopic, topic.id, updateTopic])
const onDeleteMessage = useCallback( const onDeleteMessage = useCallback(
(message: Message) => { async (message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id) const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages) setMessages(_messages)
setDisplayMessages(_messages) setDisplayMessages(_messages)
db.topics.update(topic.id, { messages: _messages }) await db.topics.update(topic.id, { messages: _messages })
deleteMessageFiles(message) await deleteMessageFiles(message)
},
[messages, topic.id]
)
const onDeleteGroupMessages = useCallback(
async (askId: string) => {
const _messages = messages.filter((m) => m.askId !== askId && m.id !== askId)
setMessages(_messages)
setDisplayMessages(_messages)
await db.topics.update(topic.id, { messages: _messages })
for (const message of _messages) {
await deleteMessageFiles(message)
}
}, },
[messages, topic.id] [messages, topic.id]
) )
@ -323,6 +301,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
hidePresetMessages={assistant.settings?.hideMessages} hidePresetMessages={assistant.settings?.hideMessages}
onSetMessages={setMessages} onSetMessages={setMessages}
onDeleteMessage={onDeleteMessage} onDeleteMessage={onDeleteMessage}
onDeleteGroupMessages={onDeleteGroupMessages}
onGetMessages={onGetMessages} onGetMessages={onGetMessages}
/> />
))} ))}
@ -334,4 +313,38 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
) )
} }
interface LoaderProps {
$loading: boolean
}
const LoaderContainer = styled.div<LoaderProps>`
display: flex;
justify-content: center;
padding: 10px;
width: 100%;
background: var(--color-background);
opacity: ${(props) => (props.$loading ? 1 : 0)};
transition: opacity 0.3s ease;
pointer-events: none;
`
const ScrollContainer = styled.div`
display: flex;
flex-direction: column-reverse;
padding: 0 20px;
`
interface ContainerProps {
$right?: boolean
}
const Container = styled(Scrollbar)<ContainerProps>`
display: flex;
flex-direction: column-reverse;
padding: 10px 0;
padding-bottom: 20px;
overflow-x: hidden;
background-color: var(--color-background);
`
export default Messages export default Messages

View File

@ -27,7 +27,6 @@ const Prompt: FC<Props> = ({ assistant }) => {
const Container = styled.div` const Container = styled.div`
padding: 10px 20px; padding: 10px 20px;
background-color: var(--color-background-soft); background-color: var(--color-background-soft);
margin-bottom: 20px;
margin: 4px 20px 0 20px; margin: 4px 20px 0 20px;
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;

View File

@ -63,9 +63,11 @@ export interface SettingsState {
narrowMode: boolean narrowMode: boolean
enableQuickAssistant: boolean enableQuickAssistant: boolean
clickTrayToShowQuickAssistant: boolean clickTrayToShowQuickAssistant: boolean
multiModelMessageStyle: 'horizontal' | 'vertical' | 'fold' multiModelMessageStyle: MultiModelMessageStyle
} }
export type MultiModelMessageStyle = 'horizontal' | 'vertical' | 'fold'
const initialState: SettingsState = { const initialState: SettingsState = {
showAssistants: true, showAssistants: true,
showTopics: true, showTopics: true,
@ -111,7 +113,7 @@ const initialState: SettingsState = {
narrowMode: false, narrowMode: false,
enableQuickAssistant: false, enableQuickAssistant: false,
clickTrayToShowQuickAssistant: false, clickTrayToShowQuickAssistant: false,
multiModelMessageStyle: 'vertical' multiModelMessageStyle: 'horizontal'
} }
const settingsSlice = createSlice({ const settingsSlice = createSlice({