refactor: Extract message group menu bar into a separate component

This commit is contained in:
kangfenmao 2025-02-17 23:21:24 +08:00
parent bc77c423b3
commit 0c13e708b9
2 changed files with 223 additions and 183 deletions

View File

@ -1,24 +1,14 @@
import {
ColumnHeightOutlined,
ColumnWidthOutlined,
DeleteOutlined,
FolderOutlined,
NumberOutlined
} 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 { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import { Message, Model, Topic } from '@renderer/types'
import { Button, Popover, Segmented as AntdSegmented } from 'antd'
import { Dispatch, FC, SetStateAction, useEffect, useState } from 'react'
import { Message, Topic } from '@renderer/types'
import { Popover } from 'antd'
import { Dispatch, FC, memo, SetStateAction, useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import styled, { css } from 'styled-components'
import MessageItem from './Message'
import MessageGroupSettings from './MessageGroupSettings'
import MessageGroupMenuBar from './MessageGroupMenuBar'
interface Props {
messages: (Message & { index: number })[]
@ -39,7 +29,7 @@ const MessageGroup: FC<Props> = ({
onGetMessages,
onDeleteGroupMessages
}) => {
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns } = useSettings()
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
const { t } = useTranslation()
const [multiModelMessageStyle, setMultiModelMessageStyle] =
@ -48,11 +38,10 @@ const MessageGroup: FC<Props> = ({
const messageLength = messages.length
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
const { gridPopoverTrigger } = useSettings()
const isGrouped = messageLength > 1
const isHorizontal = multiModelMessageStyle === 'horizontal'
const onDelete = async () => {
const onDelete = useCallback(async () => {
window.modal.confirm({
title: t('message.group.delete.title'),
content: t('message.group.delete.content'),
@ -66,26 +55,50 @@ const MessageGroup: FC<Props> = ({
askId && onDeleteGroupMessages?.(askId)
}
})
}
}, [messages, onDeleteGroupMessages, t])
useEffect(() => {
setSelectedIndex(messageLength - 1)
}, [messageLength])
const isHorizontal = multiModelMessageStyle === 'horizontal'
return (
<GroupContainer $isGrouped={isGrouped} $layout={multiModelMessageStyle}>
<GridContainer $count={messageLength} $layout={multiModelMessageStyle} $gridColumns={gridColumns}>
{messages.map((message, index) =>
multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped ? (
<Popover
content={
{messages.map((message, index) => {
const isGridGroupMessage = multiModelMessageStyle === 'grid' && message.role === 'assistant' && isGrouped
if (isGridGroupMessage) {
return (
<Popover
content={
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
$isInPopover={true}
key={message.id}>
<MessageItem
isGrouped={isGrouped}
message={message}
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
style={{
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
}}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
}
trigger={gridPopoverTrigger}
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}
key={message.id}>
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
$isInPopover={true}
key={message.id}>
<MessageItem
isGrouped={isGrouped}
@ -93,46 +106,24 @@ const MessageGroup: FC<Props> = ({
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
style={{
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15
}}
style={
gridPopoverTrigger === 'hover' && isGrouped
? {
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15,
overflow: isGrouped ? 'hidden' : 'auto',
maxHeight: isGrouped ? '280px' : 'unset'
}
: undefined
}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
}
trigger={gridPopoverTrigger}
styles={{ root: { maxWidth: '60vw', minWidth: '550px', overflowY: 'auto', zIndex: 1000 } }}
getPopupContainer={(triggerNode) => triggerNode.parentNode as HTMLElement}
key={message.id}>
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
$isGrouped={isGrouped}
key={message.id}>
<MessageItem
isGrouped={isGrouped}
message={message}
topic={topic}
index={message.index}
hidePresetMessages={hidePresetMessages}
style={
gridPopoverTrigger === 'hover' && isGrouped
? {
paddingTop: isGrouped && ['horizontal', 'grid'].includes(multiModelMessageStyle) ? 0 : 15,
overflow: isGrouped ? 'hidden' : 'auto',
maxHeight: isGrouped ? '280px' : 'unset'
}
: undefined
}
onSetMessages={onSetMessages}
onDeleteMessage={onDeleteMessage}
onGetMessages={onGetMessages}
/>
</MessageWrapper>
</Popover>
) : (
</Popover>
)
}
return (
<MessageWrapper
$layout={multiModelMessageStyle}
$selected={index === selectedIndex}
@ -152,59 +143,17 @@ const MessageGroup: FC<Props> = ({
/>
</MessageWrapper>
)
)}
})}
</GridContainer>
{isGrouped && (
<GroupMenuBar className="group-menu-bar" $layout={multiModelMessageStyle}>
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
<LayoutContainer>
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
<LayoutOption
key={layout}
active={multiModelMessageStyle === layout}
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}>
{layout === 'fold' ? (
<FolderOutlined />
) : layout === 'horizontal' ? (
<ColumnWidthOutlined />
) : layout === 'vertical' ? (
<ColumnHeightOutlined />
) : (
<NumberOutlined />
)}
</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>
)}
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
</HStack>
<Button
type="text"
size="small"
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
onClick={onDelete}
/>
</GroupMenuBar>
<MessageGroupMenuBar
multiModelMessageStyle={multiModelMessageStyle}
setMultiModelMessageStyle={setMultiModelMessageStyle}
messages={messages}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
onDelete={onDelete}
/>
)}
</GroupContainer>
)
@ -288,74 +237,4 @@ const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
`}
`
const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 6px 10px;
border-radius: 6px;
margin-top: 10px;
justify-content: space-between;
overflow: hidden;
border: 0.5px solid var(--color-border);
height: 40px;
margin-left: ${({ $layout }) => (['horizontal', 'grid'].includes($layout) ? '0' : '40px')};
transition: all 0.3s ease;
`
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-background-soft)' : 'transparent')};
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'var(--color-hover)')};
}
`
const ModelsContainer = styled(Scrollbar)`
display: flex;
flex-direction: column;
justify-content: space-between;
&::-webkit-scrollbar {
display: none;
}
`
const Segmented = styled(AntdSegmented)`
.ant-segmented-item {
background-color: transparent !important;
transition: none !important;
&:hover {
background: transparent !important;
}
}
.ant-segmented-thumb,
.ant-segmented-item-selected {
background-color: transparent !important;
border: 0.5px solid var(--color-border);
transition: none !important;
}
`
const SegmentedLabel = styled.div`
display: flex;
align-items: center;
gap: 5px;
padding: 3px 0;
`
const ModelName = styled.span`
font-weight: 500;
font-size: 12px;
`
export default MessageGroup
export default memo(MessageGroup)

View File

@ -0,0 +1,161 @@
import {
ColumnHeightOutlined,
ColumnWidthOutlined,
DeleteOutlined,
FolderOutlined,
NumberOutlined
} from '@ant-design/icons'
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
import { HStack } from '@renderer/components/Layout'
import Scrollbar from '@renderer/components/Scrollbar'
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import { MultiModelMessageStyle } from '@renderer/store/settings'
import { Message, Model } from '@renderer/types'
import { Button, Segmented as AntdSegmented } from 'antd'
import { FC, memo } from 'react'
import styled from 'styled-components'
import MessageGroupSettings from './MessageGroupSettings'
interface Props {
multiModelMessageStyle: MultiModelMessageStyle
setMultiModelMessageStyle: (style: MultiModelMessageStyle) => void
messages: Message[]
selectedIndex: number
setSelectedIndex: (index: number) => void
onDelete: () => void
}
const MessageGroupMenuBar: FC<Props> = ({
multiModelMessageStyle,
setMultiModelMessageStyle,
messages,
selectedIndex,
setSelectedIndex,
onDelete
}) => {
return (
<GroupMenuBar $layout={multiModelMessageStyle}>
<HStack style={{ alignItems: 'center', flex: 1, overflow: 'hidden' }}>
<LayoutContainer>
{['fold', 'vertical', 'horizontal', 'grid'].map((layout) => (
<LayoutOption
key={layout}
active={multiModelMessageStyle === layout}
onClick={() => setMultiModelMessageStyle(layout as MultiModelMessageStyle)}>
{layout === 'fold' ? (
<FolderOutlined />
) : layout === 'horizontal' ? (
<ColumnWidthOutlined />
) : layout === 'vertical' ? (
<ColumnHeightOutlined />
) : (
<NumberOutlined />
)}
</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>
)}
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
</HStack>
<Button
type="text"
size="small"
icon={<DeleteOutlined style={{ color: 'var(--color-error)' }} />}
onClick={onDelete}
/>
</GroupMenuBar>
)
}
const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 6px 10px;
border-radius: 6px;
margin-top: 10px;
justify-content: space-between;
overflow: hidden;
border: 0.5px solid var(--color-border);
height: 40px;
margin-left: ${({ $layout }) => (['horizontal', 'grid'].includes($layout) ? '0' : '40px')};
transition: all 0.3s ease;
`
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-background-soft)' : 'transparent')};
&:hover {
background-color: ${({ active }) => (active ? 'var(--color-background-soft)' : 'var(--color-hover)')};
}
`
const ModelsContainer = styled(Scrollbar)`
display: flex;
flex-direction: column;
justify-content: space-between;
&::-webkit-scrollbar {
display: none;
}
`
const Segmented = styled(AntdSegmented)`
.ant-segmented-item {
background-color: transparent !important;
transition: none !important;
&:hover {
background: transparent !important;
}
}
.ant-segmented-thumb,
.ant-segmented-item-selected {
background-color: transparent !important;
border: 0.5px solid var(--color-border);
transition: none !important;
}
`
const SegmentedLabel = styled.div`
display: flex;
align-items: center;
gap: 5px;
padding: 3px 0;
`
const ModelName = styled.span`
font-weight: 500;
font-size: 12px;
`
export default memo(MessageGroupMenuBar)