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
total?: number
hidePresetMessages?: boolean
style?: React.CSSProperties
onGetMessages?: () => Message[]
onSetMessages?: Dispatch<SetStateAction<Message[]>>
onDeleteMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => Promise<void>
}
const getMessageBackground = (isBubbleStyle: boolean, isAssistantMessage: boolean) =>
@ -38,6 +39,7 @@ const MessageItem: FC<Props> = ({
topic,
index,
hidePresetMessages,
style,
onDeleteMessage,
onSetMessages,
onGetMessages
@ -123,7 +125,7 @@ const MessageItem: FC<Props> = ({
onResponse: (msg) => {
setMessage(msg)
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)
db.topics.update(topic.id, { messages: _messages })
}
@ -157,7 +159,7 @@ const MessageItem: FC<Props> = ({
'message-user': !isAssistantMessage
})}
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} />
<MessageContentContainer
className="message-content-container"

View File

@ -38,7 +38,7 @@ const MessageContent: React.FC<{
return (
<>
<Flex gap="8px" wrap>
<Flex gap="8px" wrap style={{ marginBottom: 10 }}>
{message.mentions?.map((model) => <MentionTag key={model.id}>{'@' + model.name}</MentionTag>)}
</Flex>
<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 { Message, Topic } from '@renderer/types'
import { Segmented } from 'antd'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
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 styled from 'styled-components'
import styled, { css } from 'styled-components'
import MessageItem from './Message'
@ -12,7 +18,8 @@ interface Props {
hidePresetMessages?: boolean
onGetMessages?: () => Message[]
onSetMessages?: Dispatch<SetStateAction<Message[]>>
onDeleteMessage?: (message: Message) => void
onDeleteMessage?: (message: Message) => Promise<void>
onDeleteGroupMessages?: (askId: string) => Promise<void>
}
const MessageGroup: FC<Props> = ({
@ -21,33 +28,39 @@ const MessageGroup: FC<Props> = ({
hidePresetMessages,
onDeleteMessage,
onSetMessages,
onGetMessages
onGetMessages,
onDeleteGroupMessages
}) => {
const { multiModelMessageStyle } = useSettings()
const { multiModelMessageStyle: multiModelMessageStyleSetting } = useSettings()
const [multiModelMessageStyle, setMultiModelMessageStyle] =
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
const messageLength = messages.length
const [selectedIndex, setSelectedIndex] = useState(0)
const isGrouped = messageLength > 1
const onDelete = async () => {
const askId = messages[0].askId
askId && onDeleteGroupMessages?.(askId)
}
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"
/>
)}
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
<GridContainer $count={messageLength} $layout={multiModelMessageStyle}>
{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
message={message}
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
style={{ paddingTop: isGrouped && multiModelMessageStyle === 'horizontal' ? 0 : 15 }}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
@ -55,24 +68,80 @@ const MessageGroup: FC<Props> = ({
</MessageWrapper>
))}
</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>
)
}
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%;
overflow-x: auto;
display: grid;
grid-template-columns: repeat(
${(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%;
display: ${(props) => {
if (props.$layout === 'fold') {
@ -83,6 +152,69 @@ const MessageWrapper = styled.div<{ $layout: 'fold' | 'horizontal' | 'vertical';
}
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

View File

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

View File

@ -36,41 +36,6 @@ interface Props {
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, setMessages] = 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])
const onDeleteMessage = useCallback(
(message: Message) => {
async (message: Message) => {
const _messages = messages.filter((m) => m.id !== message.id)
setMessages(_messages)
setDisplayMessages(_messages)
db.topics.update(topic.id, { messages: _messages })
deleteMessageFiles(message)
await db.topics.update(topic.id, { messages: _messages })
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]
)
@ -323,6 +301,7 @@ const Messages: FC<Props> = ({ assistant, topic, setActiveTopic }) => {
hidePresetMessages={assistant.settings?.hideMessages}
onSetMessages={setMessages}
onDeleteMessage={onDeleteMessage}
onDeleteGroupMessages={onDeleteGroupMessages}
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

View File

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

View File

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