diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index 37625728..689251b6 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -402,6 +402,8 @@ "message.multi_model_style.grid": "Grid layout", "message.multi_model_style.horizontal": "Side by side", "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.bubble": "Bubble", "message.style.plain": "Plain", diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index e85031d6..b95f7ece 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -402,6 +402,8 @@ "message.multi_model_style.grid": "カード表示", "message.multi_model_style.horizontal": "横並び", "message.multi_model_style.vertical": "縦積み", + "message.multi_model_style.fold.compress": "緊湊配置に切り替える", + "message.multi_model_style.fold.expand": "展開配置に切り替える", "message.style": "メッセージスタイル", "message.style.bubble": "バブル", "message.style.plain": "プレーン", diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index b5f90b92..ad8d3516 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -402,6 +402,8 @@ "message.multi_model_style.grid": "Карточки", "message.multi_model_style.horizontal": "Горизонтальное расположение", "message.multi_model_style.vertical": "Вертикальное расположение", + "message.multi_model_style.fold.compress": "Переключить на компактный макет", + "message.multi_model_style.fold.expand": "Переключить на расширенный макет", "message.style": "Стиль сообщения", "message.style.bubble": "Пузырь", "message.style.plain": "Простой", diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index c3f194be..9274a8f8 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -402,6 +402,8 @@ "message.multi_model_style.grid": "卡片布局", "message.multi_model_style.horizontal": "横向排列", "message.multi_model_style.vertical": "纵向堆叠", + "message.multi_model_style.fold.compress": "切换到紧凑排列", + "message.multi_model_style.fold.expand": "切换到展开排列", "message.style": "消息样式", "message.style.bubble": "气泡", "message.style.plain": "简洁", diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 746a7fd4..8160ade4 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -402,6 +402,8 @@ "message.multi_model_style.grid": "卡片設定", "message.multi_model_style.horizontal": "橫向排列", "message.multi_model_style.vertical": "縱向堆疊", + "message.multi_model_style.fold.compress": "切換到緊湊排列", + "message.multi_model_style.fold.expand": "切換到展開排列", "message.style": "訊息樣式", "message.style.bubble": "氣泡", "message.style.plain": "簡潔", diff --git a/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx b/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx index 859a2372..da272d14 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroupMenuBar.tsx @@ -5,17 +5,15 @@ import { 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, Tooltip } from 'antd' +import { Message } from '@renderer/types' +import { Button, Tooltip } from 'antd' import { FC, memo } from 'react' import { useTranslation } from 'react-i18next' import styled from 'styled-components' +import MessageGroupModelList from './MessageGroupModelList' import MessageGroupSettings from './MessageGroupSettings' interface Props { @@ -61,25 +59,11 @@ const MessageGroupMenuBar: FC = ({ ))} {multiModelMessageStyle === 'fold' && ( - - { - setSelectedIndex(Number(value)) - EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false) - }} - options={messages.map((message, index) => ({ - label: ( - - - {message.model?.name} - - ), - value: index.toString() - }))} - size="small" - /> - + )} {multiModelMessageStyle === 'grid' && } @@ -111,13 +95,13 @@ const GroupMenuBar = styled.div<{ $layout: MultiModelMessageStyle }>` const LayoutContainer = styled.div` display: flex; - gap: 10px; + gap: 4px; flex-direction: row; ` const LayoutOption = styled.div<{ $active: boolean }>` cursor: pointer; - padding: 2px 10px; + padding: 2px 6px; border-radius: 4px; 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) diff --git a/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx new file mode 100644 index 00000000..9c1d6925 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/MessageGroupModelList.tsx @@ -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 = ({ messages, selectedIndex, setSelectedIndex }) => { + const { t } = useTranslation() + const [displayMode, setDisplayMode] = useState('expanded') + + return ( + + + + {displayMode === 'compact' ? ( + setDisplayMode('expanded')} /> + ) : ( + setDisplayMode('compact')} /> + )} + + + + + {displayMode === 'compact' ? ( + /* Compact style display */ + + {messages.map((message, index) => ( + + { + setSelectedIndex(index) + EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[index].id, false) + }}> + + + + ))} + + ) : ( + /* Expanded style display */ + { + setSelectedIndex(Number(value)) + EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false) + }} + options={messages.map((message, index) => ({ + label: ( + + + {message.model?.name} + + ), + value: index.toString() + }))} + size="small" + /> + )} + + + ) +} + +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