feat: add group message action bar
This commit is contained in:
parent
ccac5358f4
commit
dd464db594
@ -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"
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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({
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user