feat(message): add a compact style for the model list in message groups (#2962)
* feat(message): add a compact style for the model list in message groups * refactor: use button as action rather than state --------- Co-authored-by: 亢奋猫 <kangfenmao@qq.com>
This commit is contained in:
parent
8e36d29996
commit
5c4f0e8e8e
@ -402,6 +402,8 @@
|
|||||||
"message.multi_model_style.grid": "Grid layout",
|
"message.multi_model_style.grid": "Grid layout",
|
||||||
"message.multi_model_style.horizontal": "Side by side",
|
"message.multi_model_style.horizontal": "Side by side",
|
||||||
"message.multi_model_style.vertical": "Stacked view",
|
"message.multi_model_style.vertical": "Stacked view",
|
||||||
|
"message.multi_model_style.fold.compress": "Switch to compact layout",
|
||||||
|
"message.multi_model_style.fold.expand": "Switch to expanded layout",
|
||||||
"message.style": "Message style",
|
"message.style": "Message style",
|
||||||
"message.style.bubble": "Bubble",
|
"message.style.bubble": "Bubble",
|
||||||
"message.style.plain": "Plain",
|
"message.style.plain": "Plain",
|
||||||
|
|||||||
@ -402,6 +402,8 @@
|
|||||||
"message.multi_model_style.grid": "カード表示",
|
"message.multi_model_style.grid": "カード表示",
|
||||||
"message.multi_model_style.horizontal": "横並び",
|
"message.multi_model_style.horizontal": "横並び",
|
||||||
"message.multi_model_style.vertical": "縦積み",
|
"message.multi_model_style.vertical": "縦積み",
|
||||||
|
"message.multi_model_style.fold.compress": "緊湊配置に切り替える",
|
||||||
|
"message.multi_model_style.fold.expand": "展開配置に切り替える",
|
||||||
"message.style": "メッセージスタイル",
|
"message.style": "メッセージスタイル",
|
||||||
"message.style.bubble": "バブル",
|
"message.style.bubble": "バブル",
|
||||||
"message.style.plain": "プレーン",
|
"message.style.plain": "プレーン",
|
||||||
|
|||||||
@ -402,6 +402,8 @@
|
|||||||
"message.multi_model_style.grid": "Карточки",
|
"message.multi_model_style.grid": "Карточки",
|
||||||
"message.multi_model_style.horizontal": "Горизонтальное расположение",
|
"message.multi_model_style.horizontal": "Горизонтальное расположение",
|
||||||
"message.multi_model_style.vertical": "Вертикальное расположение",
|
"message.multi_model_style.vertical": "Вертикальное расположение",
|
||||||
|
"message.multi_model_style.fold.compress": "Переключить на компактный макет",
|
||||||
|
"message.multi_model_style.fold.expand": "Переключить на расширенный макет",
|
||||||
"message.style": "Стиль сообщения",
|
"message.style": "Стиль сообщения",
|
||||||
"message.style.bubble": "Пузырь",
|
"message.style.bubble": "Пузырь",
|
||||||
"message.style.plain": "Простой",
|
"message.style.plain": "Простой",
|
||||||
|
|||||||
@ -402,6 +402,8 @@
|
|||||||
"message.multi_model_style.grid": "卡片布局",
|
"message.multi_model_style.grid": "卡片布局",
|
||||||
"message.multi_model_style.horizontal": "横向排列",
|
"message.multi_model_style.horizontal": "横向排列",
|
||||||
"message.multi_model_style.vertical": "纵向堆叠",
|
"message.multi_model_style.vertical": "纵向堆叠",
|
||||||
|
"message.multi_model_style.fold.compress": "切换到紧凑排列",
|
||||||
|
"message.multi_model_style.fold.expand": "切换到展开排列",
|
||||||
"message.style": "消息样式",
|
"message.style": "消息样式",
|
||||||
"message.style.bubble": "气泡",
|
"message.style.bubble": "气泡",
|
||||||
"message.style.plain": "简洁",
|
"message.style.plain": "简洁",
|
||||||
|
|||||||
@ -402,6 +402,8 @@
|
|||||||
"message.multi_model_style.grid": "卡片設定",
|
"message.multi_model_style.grid": "卡片設定",
|
||||||
"message.multi_model_style.horizontal": "橫向排列",
|
"message.multi_model_style.horizontal": "橫向排列",
|
||||||
"message.multi_model_style.vertical": "縱向堆疊",
|
"message.multi_model_style.vertical": "縱向堆疊",
|
||||||
|
"message.multi_model_style.fold.compress": "切換到緊湊排列",
|
||||||
|
"message.multi_model_style.fold.expand": "切換到展開排列",
|
||||||
"message.style": "訊息樣式",
|
"message.style": "訊息樣式",
|
||||||
"message.style.bubble": "氣泡",
|
"message.style.bubble": "氣泡",
|
||||||
"message.style.plain": "簡潔",
|
"message.style.plain": "簡潔",
|
||||||
|
|||||||
@ -5,17 +5,15 @@ import {
|
|||||||
FolderOutlined,
|
FolderOutlined,
|
||||||
NumberOutlined
|
NumberOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
|
||||||
import { HStack } from '@renderer/components/Layout'
|
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 { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||||
import { Message, Model } from '@renderer/types'
|
import { Message } from '@renderer/types'
|
||||||
import { Button, Segmented as AntdSegmented, Tooltip } from 'antd'
|
import { Button, Tooltip } from 'antd'
|
||||||
import { FC, memo } from 'react'
|
import { FC, memo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import MessageGroupModelList from './MessageGroupModelList'
|
||||||
import MessageGroupSettings from './MessageGroupSettings'
|
import MessageGroupSettings from './MessageGroupSettings'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -61,25 +59,11 @@ const MessageGroupMenuBar: FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
{multiModelMessageStyle === 'fold' && (
|
{multiModelMessageStyle === 'fold' && (
|
||||||
<ModelsContainer>
|
<MessageGroupModelList
|
||||||
<Segmented
|
messages={messages}
|
||||||
value={selectedIndex.toString()}
|
selectedIndex={selectedIndex}
|
||||||
onChange={(value) => {
|
setSelectedIndex={setSelectedIndex}
|
||||||
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 />}
|
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
||||||
</HStack>
|
</HStack>
|
||||||
@ -111,13 +95,13 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>`
|
|||||||
|
|
||||||
const LayoutContainer = styled.div`
|
const LayoutContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 4px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
`
|
`
|
||||||
|
|
||||||
const LayoutOption = styled.div<{ $active: boolean }>`
|
const LayoutOption = styled.div<{ $active: boolean }>`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 2px 10px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: ${({ $active }) => ($active ? 'var(--color-background-soft)' : 'transparent')};
|
background-color: ${({ $active }) => ($active ? 'var(--color-background-soft)' : 'transparent')};
|
||||||
|
|
||||||
@ -126,48 +110,4 @@ const LayoutOption = styled.div<{ $active: boolean }>`
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
const ModelsContainer = styled(Scrollbar)`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
&::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
const Segmented = styled(AntdSegmented)`
|
|
||||||
&.ant-segmented {
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
.ant-segmented-item {
|
|
||||||
background-color: transparent !important;
|
|
||||||
transition: none !important;
|
|
||||||
border-radius: var(--list-item-border-radius) !important;
|
|
||||||
box-shadow: 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;
|
|
||||||
border-radius: var(--list-item-border-radius) !important;
|
|
||||||
box-shadow: 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)
|
export default memo(MessageGroupMenuBar)
|
||||||
|
|||||||
255
src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx
Normal file
255
src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
import { ArrowsAltOutlined, ShrinkOutlined } from '@ant-design/icons'
|
||||||
|
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||||
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
|
import { Message, Model } from '@renderer/types'
|
||||||
|
import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd'
|
||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
interface MessageGroupModelListProps {
|
||||||
|
messages: Message[]
|
||||||
|
selectedIndex: number
|
||||||
|
setSelectedIndex: (index: number) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
type DisplayMode = 'compact' | 'expanded'
|
||||||
|
|
||||||
|
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selectedIndex, setSelectedIndex }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [displayMode, setDisplayMode] = useState<DisplayMode>('expanded')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModelsWrapper>
|
||||||
|
<DisplayModeToggle displayMode={displayMode}>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
displayMode === 'compact'
|
||||||
|
? t('message.message.multi_model_style.fold.expand')
|
||||||
|
: t('message.message.multi_model_style.fold.compress')
|
||||||
|
}
|
||||||
|
placement="top">
|
||||||
|
{displayMode === 'compact' ? (
|
||||||
|
<ArrowsAltOutlined onClick={() => setDisplayMode('expanded')} />
|
||||||
|
) : (
|
||||||
|
<ShrinkOutlined onClick={() => setDisplayMode('compact')} />
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</DisplayModeToggle>
|
||||||
|
|
||||||
|
<ModelsContainer $displayMode={displayMode}>
|
||||||
|
{displayMode === 'compact' ? (
|
||||||
|
/* Compact style display */
|
||||||
|
<Avatar.Group className="avatar-group">
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<Tooltip key={index} title={message.model?.name} placement="top" mouseEnterDelay={0.2}>
|
||||||
|
<AvatarWrapper
|
||||||
|
className="avatar-wrapper"
|
||||||
|
isSelected={selectedIndex === index}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedIndex(index)
|
||||||
|
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[index].id, false)
|
||||||
|
}}>
|
||||||
|
<ModelAvatar model={message.model as Model} size={28} />
|
||||||
|
</AvatarWrapper>
|
||||||
|
</Tooltip>
|
||||||
|
))}
|
||||||
|
</Avatar.Group>
|
||||||
|
) : (
|
||||||
|
/* Expanded style display */
|
||||||
|
<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>
|
||||||
|
</ModelsWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModelsWrapper = styled.div`
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
`
|
||||||
|
|
||||||
|
const DisplayModeToggle = styled.div<{ displayMode: DisplayMode }>`
|
||||||
|
position: absolute;
|
||||||
|
left: 4px; /* Add more space on the left */
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 5;
|
||||||
|
width: 28px; /* Increase width */
|
||||||
|
height: 28px; /* Add height */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
|
||||||
|
/* Add hover effect */
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-hover);
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const ModelsContainer = styled(Scrollbar)<{ $displayMode: DisplayMode }>`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: ${(props) => (props.$displayMode === 'expanded' ? 'column' : 'row')};
|
||||||
|
justify-content: ${(props) => (props.$displayMode === 'expanded' ? 'space-between' : 'flex-start')};
|
||||||
|
align-items: center;
|
||||||
|
overflow-x: auto;
|
||||||
|
flex: 1;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-left: 24px; /* Space for toggle button */
|
||||||
|
|
||||||
|
/* Hide scrollbar to match original code */
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card mode styles */
|
||||||
|
.avatar-group.ant-avatar-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
position: relative;
|
||||||
|
padding: 6px 4px;
|
||||||
|
|
||||||
|
/* Base style - default overlapping effect */
|
||||||
|
& > * {
|
||||||
|
margin-left: -6px !important;
|
||||||
|
/* Separate transition properties to avoid conflicts */
|
||||||
|
transition:
|
||||||
|
transform 0.18s ease-out,
|
||||||
|
margin 0.18s ease-out !important;
|
||||||
|
position: relative;
|
||||||
|
/* Only use will-change for transform to reduce rendering overhead */
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* First element has no left margin */
|
||||||
|
& > *:first-child {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Using :has() selector to handle the element before the hovered one */
|
||||||
|
& > *:has(+ *:hover) {
|
||||||
|
margin-right: 2px !important;
|
||||||
|
/* Use transform instead of margin to reduce layout recalculations */
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Element after the hovered one */
|
||||||
|
& > *:hover + * {
|
||||||
|
margin-left: 5px !important;
|
||||||
|
/* Avoid transform here to prevent jittering */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Second element after the hovered one */
|
||||||
|
& > *:hover + * + * {
|
||||||
|
margin-left: -4px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const AvatarWrapper = styled.div<{ isSelected: boolean }>`
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
border-radius: 50%;
|
||||||
|
/* Keep z-index separate from transitions to avoid rendering issues */
|
||||||
|
z-index: ${(props) => (props.isSelected ? 2 : 0)};
|
||||||
|
background: var(--color-background);
|
||||||
|
/* Simplify transitions to reduce jittering */
|
||||||
|
transition:
|
||||||
|
transform 0.18s ease-out,
|
||||||
|
margin 0.18s ease-out,
|
||||||
|
box-shadow 0.18s ease-out,
|
||||||
|
filter 0.18s ease-out;
|
||||||
|
box-shadow: 0 0 0 1px var(--color-background);
|
||||||
|
|
||||||
|
/* Use CSS variables to define animation parameters for easy adjustment */
|
||||||
|
--hover-scale: 1.15;
|
||||||
|
--hover-x-offset: 6px;
|
||||||
|
--hover-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
/* z-index is applied immediately, not part of the transition */
|
||||||
|
z-index: 10;
|
||||||
|
transform: translateX(var(--hover-x-offset)) scale(var(--hover-scale));
|
||||||
|
box-shadow: var(--hover-shadow);
|
||||||
|
filter: brightness(1.02);
|
||||||
|
margin-left: 8px !important;
|
||||||
|
margin-right: 4px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
props.isSelected &&
|
||||||
|
`
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
z-index: 2;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
/* z-index is applied immediately, not part of the transition */
|
||||||
|
z-index: 10;
|
||||||
|
border: 2px solid var(--color-primary);
|
||||||
|
filter: brightness(1.02);
|
||||||
|
transform: translateX(var(--hover-x-offset)) scale(var(--hover-scale));
|
||||||
|
margin-left: 8px !important;
|
||||||
|
margin-right: 4px !important;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`
|
||||||
|
|
||||||
|
const Segmented = styled(AntdSegmented)`
|
||||||
|
width: 100%;
|
||||||
|
background-color: transparent !important;
|
||||||
|
|
||||||
|
.ant-segmented-item {
|
||||||
|
background-color: transparent !important;
|
||||||
|
transition: none !important;
|
||||||
|
border-radius: var(--list-item-border-radius) !important;
|
||||||
|
box-shadow: 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;
|
||||||
|
border-radius: var(--list-item-border-radius) !important;
|
||||||
|
box-shadow: 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 MessageGroupModelList
|
||||||
Loading…
x
Reference in New Issue
Block a user