feat(Messages): 新增消息锚点功能,在右侧显示消息大纲 (#3674)
* feat(Messages): 添加消息线和分页按钮的设置选项 * feat(Messages): 将“消息线”更名为“对话锚点”,并更新相关设置和国际化文本 * fix(Messages): 调整消息透明度和缩放效果,优化消息项的样式和交互 * feat(Messages): 添加容器高度自适应功能,优化消息线样式和交互效果 * fix(Messages): 调整消息容器高度计算和样式,优化交互效果 * feat(settings): 更新消息导航相关翻译和设置
This commit is contained in:
parent
998c4bc459
commit
852274b4b1
@ -926,6 +926,10 @@
|
||||
"jsonSaveError": "Failed to save JSON configuration."
|
||||
},
|
||||
"messages.divider": "Show divider between messages",
|
||||
"messages.navigation": "Message Navigation",
|
||||
"messages.navigation.none": "None",
|
||||
"messages.navigation.buttons": "Navigation Buttons",
|
||||
"messages.navigation.anchor": "Message Anchor",
|
||||
"messages.grid_columns": "Message grid display columns",
|
||||
"messages.grid_popover_trigger": "Grid detail trigger",
|
||||
"messages.grid_popover_trigger.click": "Click to display",
|
||||
|
||||
@ -926,6 +926,10 @@
|
||||
"jsonSaveError": "JSON設定の保存に失敗しました"
|
||||
},
|
||||
"messages.divider": "メッセージ間に区切り線を表示",
|
||||
"messages.navigation": "メッセージナビゲーション",
|
||||
"messages.navigation.none": "表示しない",
|
||||
"messages.navigation.buttons": "上下ボタン",
|
||||
"messages.navigation.anchor": "会話アンカー",
|
||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
||||
"messages.grid_popover_trigger.click": "クリックで表示",
|
||||
|
||||
@ -926,6 +926,10 @@
|
||||
"jsonSaveError": "Не удалось сохранить конфигурацию JSON"
|
||||
},
|
||||
"messages.divider": "Показывать разделитель между сообщениями",
|
||||
"messages.navigation": "Навигация сообщений",
|
||||
"messages.navigation.none": "Не показывать",
|
||||
"messages.navigation.buttons": "Кнопки пагинации",
|
||||
"messages.navigation.anchor": "Диалог анкор",
|
||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
||||
"messages.grid_popover_trigger.click": "Нажатие для отображения",
|
||||
|
||||
@ -926,6 +926,10 @@
|
||||
"jsonSaveError": "保存JSON配置失败"
|
||||
},
|
||||
"messages.divider": "消息分割线",
|
||||
"messages.navigation": "对话导航按钮",
|
||||
"messages.navigation.none": "不显示",
|
||||
"messages.navigation.buttons": "上下按钮",
|
||||
"messages.navigation.anchor": "对话锚点",
|
||||
"messages.grid_columns": "消息网格展示列数",
|
||||
"messages.grid_popover_trigger": "网格详情触发",
|
||||
"messages.grid_popover_trigger.click": "点击显示",
|
||||
|
||||
@ -926,6 +926,10 @@
|
||||
"jsonSaveError": "保存JSON配置失敗"
|
||||
},
|
||||
"messages.divider": "訊息間顯示分隔線",
|
||||
"messages.navigation": "訊息導航",
|
||||
"messages.navigation.none": "不顯示",
|
||||
"messages.navigation.buttons": "上下按鈕",
|
||||
"messages.navigation.anchor": "對話錨點",
|
||||
"messages.grid_columns": "訊息網格展示列數",
|
||||
"messages.grid_popover_trigger": "網格詳細資訊觸發",
|
||||
"messages.grid_popover_trigger.click": "點選顯示",
|
||||
|
||||
304
src/renderer/src/pages/home/Messages/MessageAnchorLine.tsx
Normal file
304
src/renderer/src/pages/home/Messages/MessageAnchorLine.tsx
Normal file
@ -0,0 +1,304 @@
|
||||
import { APP_NAME, AppLogo, isLocalAi } from '@renderer/config/env'
|
||||
import { getModelLogo } from '@renderer/config/models'
|
||||
import { useTheme } from '@renderer/context/ThemeProvider'
|
||||
import useAvatar from '@renderer/hooks/useAvatar'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { getMessageModelId } from '@renderer/services/MessagesService'
|
||||
import { getModelName } from '@renderer/services/ModelService'
|
||||
import { useAppDispatch } from '@renderer/store'
|
||||
import { updateMessage } from '@renderer/store/messages'
|
||||
import { Message } from '@renderer/types'
|
||||
import { isEmoji, removeLeadingEmoji } from '@renderer/utils'
|
||||
import { Avatar } from 'antd'
|
||||
import { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import styled from 'styled-components'
|
||||
interface MessageLineProps {
|
||||
messages: Message[]
|
||||
}
|
||||
|
||||
const getAvatarSource = (isLocalAi: boolean, modelId: string | undefined) => {
|
||||
if (isLocalAi) return AppLogo
|
||||
return modelId ? getModelLogo(modelId) : undefined
|
||||
}
|
||||
|
||||
const MessageAnchorLine: FC<MessageLineProps> = ({ messages }) => {
|
||||
const { t } = useTranslation()
|
||||
const avatar = useAvatar()
|
||||
const { theme } = useTheme()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { userName } = useSettings()
|
||||
const messagesListRef = useRef<HTMLDivElement>(null)
|
||||
const messageItemsRef = useRef<Map<string, HTMLDivElement>>(new Map())
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [mouseY, setMouseY] = useState<number | null>(null)
|
||||
const { topicPosition, showTopics } = useSettings()
|
||||
const showRightTopics = topicPosition === 'right' && showTopics
|
||||
const right = showRightTopics ? 'calc(var(--topic-list-width) + 15px)' : '15px'
|
||||
|
||||
const [listOffsetY, setListOffsetY] = useState(0)
|
||||
const [containerHeight, setContainerHeight] = useState<number | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const updateHeight = () => {
|
||||
if (containerRef.current) {
|
||||
const parentElement = containerRef.current.parentElement
|
||||
if (parentElement) {
|
||||
setContainerHeight(parentElement.clientHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateHeight()
|
||||
window.addEventListener('resize', updateHeight)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', updateHeight)
|
||||
}
|
||||
}, [messages])
|
||||
|
||||
// 函数用于计算根据距离的变化值
|
||||
const calculateValueByDistance = useCallback(
|
||||
(itemId: string, maxValue: number) => {
|
||||
if (mouseY === null) return 0
|
||||
|
||||
const element = messageItemsRef.current.get(itemId)
|
||||
if (!element) return 0
|
||||
|
||||
const rect = element.getBoundingClientRect()
|
||||
const centerY = rect.top + rect.height / 2
|
||||
const distance = Math.abs(centerY - (messagesListRef.current?.getBoundingClientRect().top || 0) - mouseY)
|
||||
const maxDistance = 100
|
||||
|
||||
return Math.max(0, maxValue * (1 - distance / maxDistance))
|
||||
},
|
||||
[mouseY]
|
||||
)
|
||||
|
||||
const getUserName = useCallback(
|
||||
(message: Message) => {
|
||||
if (isLocalAi && message.role !== 'user') {
|
||||
return APP_NAME
|
||||
}
|
||||
|
||||
if (message.role === 'assistant') {
|
||||
if (message.model) {
|
||||
return getModelName(message.model) || message.model.name || message.modelId || ''
|
||||
}
|
||||
|
||||
const modelId = getMessageModelId(message)
|
||||
return modelId || ''
|
||||
}
|
||||
|
||||
return userName || t('common.you')
|
||||
},
|
||||
[userName, t]
|
||||
)
|
||||
|
||||
const setSelectedMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const groupMessages = messages.filter((m) => m.askId === message.askId)
|
||||
if (groupMessages.length > 1) {
|
||||
groupMessages.forEach((m) => {
|
||||
dispatch(
|
||||
updateMessage({
|
||||
topicId: m.topicId,
|
||||
messageId: m.id,
|
||||
updates: { foldSelected: m.id === message.id }
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
},
|
||||
[dispatch, messages]
|
||||
)
|
||||
|
||||
const scrollToMessage = useCallback(
|
||||
(message: Message) => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
|
||||
if (!messageElement) return
|
||||
|
||||
const display = messageElement ? window.getComputedStyle(messageElement).display : null
|
||||
if (display === 'none') {
|
||||
setSelectedMessage(message)
|
||||
return
|
||||
}
|
||||
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
},
|
||||
[messages, setSelectedMessage]
|
||||
)
|
||||
|
||||
if (messages.length === 0) return null
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent) => {
|
||||
if (messagesListRef.current) {
|
||||
const containerRect = e.currentTarget.getBoundingClientRect()
|
||||
const listRect = messagesListRef.current.getBoundingClientRect()
|
||||
setMouseY(e.clientY - listRect.top)
|
||||
|
||||
if (listRect.height > containerRect.height) {
|
||||
const mousePositionRatio = (e.clientY - containerRect.top) / containerRect.height
|
||||
const maxOffset = (containerRect.height - listRect.height) / 2 - 20
|
||||
setListOffsetY(-maxOffset + mousePositionRatio * (maxOffset * 2))
|
||||
} else {
|
||||
setListOffsetY(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setMouseY(null)
|
||||
setListOffsetY(0)
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageLineContainer
|
||||
ref={containerRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
$right={right}
|
||||
$height={containerHeight}>
|
||||
<MessagesList ref={messagesListRef} style={{ transform: `translateY(${listOffsetY}px)` }}>
|
||||
{messages.map((message, index) => {
|
||||
const opacity = 0.5 + calculateValueByDistance(message.id, 1)
|
||||
const scale = 1 + calculateValueByDistance(message.id, 1)
|
||||
const size = 10 + calculateValueByDistance(message.id, 20)
|
||||
const avatarSource = getAvatarSource(isLocalAi, getMessageModelId(message))
|
||||
const username = removeLeadingEmoji(getUserName(message))
|
||||
|
||||
return (
|
||||
<MessageItem
|
||||
key={message.id}
|
||||
ref={(el) => {
|
||||
if (el) messageItemsRef.current.set(message.id, el)
|
||||
else messageItemsRef.current.delete(message.id)
|
||||
}}
|
||||
style={{
|
||||
opacity: mouseY ? opacity : Math.max(0, 0.6 - (0.3 * Math.abs(index - messages.length / 2)) / 5)
|
||||
}}
|
||||
onClick={() => scrollToMessage(message)}>
|
||||
<MessageItemContainer style={{ transform: ` scale(${scale})` }}>
|
||||
<MessageItemTitle>{username}</MessageItemTitle>
|
||||
<MessageItemContent>{message.content.substring(0, 50)}</MessageItemContent>
|
||||
</MessageItemContainer>
|
||||
|
||||
{message.role === 'assistant' ? (
|
||||
<Avatar
|
||||
src={avatarSource}
|
||||
size={size}
|
||||
style={{
|
||||
border: isLocalAi ? '1px solid var(--color-border-soft)' : 'none',
|
||||
filter: theme === 'dark' ? 'invert(0.05)' : undefined
|
||||
}}>
|
||||
A
|
||||
</Avatar>
|
||||
) : (
|
||||
<>
|
||||
{isEmoji(avatar) ? (
|
||||
<EmojiAvatar size={size}>{avatar}</EmojiAvatar>
|
||||
) : (
|
||||
<Avatar src={avatar} size={size} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</MessageItem>
|
||||
)
|
||||
})}
|
||||
</MessagesList>
|
||||
</MessageLineContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const MessageItemContainer = styled.div`
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
text-align: right;
|
||||
gap: 4px;
|
||||
text-shadow: 0 0 2px rgba(255, 255, 255, 0.5);
|
||||
opacity: 0;
|
||||
transform-origin: right center;
|
||||
`
|
||||
|
||||
const MessageLineContainer = styled.div<{ $right: string; $height: number | null }>`
|
||||
width: 14px;
|
||||
position: fixed;
|
||||
top: ${(props) => (props.$height ? `calc(${props.$height / 2}px + var(--status-bar-height))` : '50%')};
|
||||
right: ${(props) => props.$right};
|
||||
max-height: ${(props) => (props.$height ? `${props.$height}px` : 'calc(100% - var(--status-bar-height) * 2)')};
|
||||
transform: translateY(-50%);
|
||||
z-index: 0;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
font-size: 5px;
|
||||
overflow: hidden;
|
||||
&:hover {
|
||||
width: 440px;
|
||||
overflow-x: visible;
|
||||
overflow-y: hidden;
|
||||
${MessageItemContainer} {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const MessagesList = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
will-change: transform;
|
||||
`
|
||||
|
||||
const MessageItem = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transform-origin: right center;
|
||||
padding: 2px 0;
|
||||
will-change: opacity;
|
||||
opacity: 0.4;
|
||||
transition: opacity 0.1s linear;
|
||||
`
|
||||
|
||||
const MessageItemTitle = styled.div`
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
`
|
||||
const MessageItemContent = styled.div`
|
||||
color: var(--color-text-2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
`
|
||||
|
||||
const EmojiAvatar = styled.div<{ size: number }>`
|
||||
width: ${(props) => props.size}px;
|
||||
height: ${(props) => props.size}px;
|
||||
background-color: var(--color-background-soft);
|
||||
border-radius: 20%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: ${(props) => props.size * 0.6}px;
|
||||
border: 0.5px solid var(--color-border);
|
||||
`
|
||||
|
||||
export default MessageAnchorLine
|
||||
@ -1,4 +1,5 @@
|
||||
import Scrollbar from '@renderer/components/Scrollbar'
|
||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||
import { useSettings } from '@renderer/hooks/useSettings'
|
||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||
import type { Message, Topic } from '@renderer/types'
|
||||
@ -17,10 +18,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
const { editMessage } = useMessageOperations(topic)
|
||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||
|
||||
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
||||
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
|
||||
const [multiModelMessageStyle, setMultiModelMessageStyle] = useState<MultiModelMessageStyle>(
|
||||
messages[0].multiModelMessageStyle || multiModelMessageStyleSetting
|
||||
)
|
||||
|
||||
const messageLength = messages.length
|
||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||
@ -33,6 +36,22 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
setSelectedIndex(messageLength - 1)
|
||||
}, [messageLength])
|
||||
|
||||
const setSelectedMessage = useCallback(
|
||||
(message: Message) => {
|
||||
messages.forEach(async (m) => {
|
||||
await editMessage(m.id, { foldSelected: m.id === message.id })
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
const messageElement = document.getElementById(`message-${message.id}`)
|
||||
if (messageElement) {
|
||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
[editMessage, messages]
|
||||
)
|
||||
|
||||
const renderMessage = useCallback(
|
||||
(message: Message & { index: number }, index: number) => {
|
||||
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
||||
@ -49,11 +68,16 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
|
||||
const messageWrapper = (
|
||||
<MessageWrapper
|
||||
id={`message-${message.id}`}
|
||||
$layout={multiModelMessageStyle}
|
||||
$selected={index === selectedIndex}
|
||||
$isGrouped={isGrouped}
|
||||
key={message.id}
|
||||
className={message.role === 'assistant' && isHorizontal && isGrouped ? 'group-message-wrapper' : ''}>
|
||||
className={classNames({
|
||||
'group-message-wrapper': message.role === 'assistant' && isHorizontal && isGrouped,
|
||||
[multiModelMessageStyle]: true,
|
||||
selected: 'foldSelected' in message ? message.foldSelected : index === 0
|
||||
})}>
|
||||
<MessageStream {...messageProps} />
|
||||
</MessageWrapper>
|
||||
)
|
||||
@ -95,6 +119,7 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
|
||||
return (
|
||||
<GroupContainer
|
||||
id={`message-group-${messages[0].askId}`}
|
||||
$isGrouped={isGrouped}
|
||||
$layout={multiModelMessageStyle}
|
||||
className={classNames([isGrouped && 'group-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
||||
@ -108,10 +133,14 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||
{isGrouped && (
|
||||
<MessageGroupMenuBar
|
||||
multiModelMessageStyle={multiModelMessageStyle}
|
||||
setMultiModelMessageStyle={setMultiModelMessageStyle}
|
||||
setMultiModelMessageStyle={(style) => {
|
||||
setMultiModelMessageStyle(style)
|
||||
messages.forEach((message) => {
|
||||
editMessage(message.id, { multiModelMessageStyle: style })
|
||||
})
|
||||
}}
|
||||
messages={messages}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
setSelectedMessage={setSelectedMessage}
|
||||
topic={topic}
|
||||
/>
|
||||
)}
|
||||
@ -173,15 +202,18 @@ interface MessageWrapperProps {
|
||||
|
||||
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||
width: 100%;
|
||||
display: ${(props) => {
|
||||
if (props.$layout === 'fold') {
|
||||
return props.$selected ? 'block' : 'none'
|
||||
&.horizontal {
|
||||
display: inline-block;
|
||||
}
|
||||
&.grid {
|
||||
display: inline-block;
|
||||
}
|
||||
&.fold {
|
||||
display: none;
|
||||
&.selected {
|
||||
display: inline-block;
|
||||
}
|
||||
if (props.$layout === 'horizontal') {
|
||||
return 'inline-block'
|
||||
}
|
||||
return 'block'
|
||||
}};
|
||||
}
|
||||
|
||||
${({ $layout, $isGrouped }) => {
|
||||
if ($layout === 'horizontal' && $isGrouped) {
|
||||
|
||||
@ -21,8 +21,7 @@ interface Props {
|
||||
multiModelMessageStyle: MultiModelMessageStyle
|
||||
setMultiModelMessageStyle: (style: MultiModelMessageStyle) => void
|
||||
messages: Message[]
|
||||
selectedIndex: number
|
||||
setSelectedIndex: (index: number) => void
|
||||
setSelectedMessage: (message: Message) => void
|
||||
topic: Topic
|
||||
}
|
||||
|
||||
@ -30,8 +29,7 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
multiModelMessageStyle,
|
||||
setMultiModelMessageStyle,
|
||||
messages,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
setSelectedMessage,
|
||||
topic
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
@ -77,11 +75,7 @@ const MessageGroupMenuBar: FC<Props> = ({
|
||||
))}
|
||||
</LayoutContainer>
|
||||
{multiModelMessageStyle === 'fold' && (
|
||||
<MessageGroupModelList
|
||||
messages={messages}
|
||||
selectedIndex={selectedIndex}
|
||||
setSelectedIndex={setSelectedIndex}
|
||||
/>
|
||||
<MessageGroupModelList messages={messages} setSelectedMessage={setSelectedMessage} />
|
||||
)}
|
||||
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
||||
</HStack>
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
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'
|
||||
@ -10,13 +9,12 @@ import styled from 'styled-components'
|
||||
|
||||
interface MessageGroupModelListProps {
|
||||
messages: Message[]
|
||||
selectedIndex: number
|
||||
setSelectedIndex: (index: number) => void
|
||||
setSelectedMessage: (message: Message) => void
|
||||
}
|
||||
|
||||
type DisplayMode = 'compact' | 'expanded'
|
||||
|
||||
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selectedIndex, setSelectedIndex }) => {
|
||||
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, setSelectedMessage }) => {
|
||||
const { t } = useTranslation()
|
||||
const [displayMode, setDisplayMode] = useState<DisplayMode>('expanded')
|
||||
const isCompact = displayMode === 'compact'
|
||||
@ -43,10 +41,9 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selec
|
||||
<Tooltip key={index} title={message.model?.name} placement="top" mouseEnterDelay={0.2}>
|
||||
<AvatarWrapper
|
||||
className="avatar-wrapper"
|
||||
isSelected={selectedIndex === index}
|
||||
isSelected={'foldSelected' in message ? message.foldSelected! : index === 0}
|
||||
onClick={() => {
|
||||
setSelectedIndex(index)
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[index].id, false)
|
||||
setSelectedMessage(message)
|
||||
}}>
|
||||
<ModelAvatar model={message.model as Model} size={28} />
|
||||
</AvatarWrapper>
|
||||
@ -56,19 +53,19 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selec
|
||||
) : (
|
||||
/* Expanded style display */
|
||||
<Segmented
|
||||
value={selectedIndex.toString()}
|
||||
value={messages.find((message) => message.foldSelected)?.id || messages[0].id}
|
||||
onChange={(value) => {
|
||||
setSelectedIndex(Number(value))
|
||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
|
||||
const message = messages.find((message) => message.id === value) as Message
|
||||
setSelectedMessage(message)
|
||||
}}
|
||||
options={messages.map((message, index) => ({
|
||||
options={messages.map((message) => ({
|
||||
label: (
|
||||
<SegmentedLabel>
|
||||
<ModelAvatar model={message.model as Model} size={20} />
|
||||
<ModelName>{message.model?.name}</ModelName>
|
||||
</SegmentedLabel>
|
||||
),
|
||||
value: index.toString()
|
||||
value: message.id
|
||||
}))}
|
||||
size="small"
|
||||
/>
|
||||
|
||||
@ -27,6 +27,7 @@ import styled from 'styled-components'
|
||||
|
||||
import ChatNavigation from './ChatNavigation'
|
||||
import MessageGroup from './MessageGroup'
|
||||
import MessageAnchorLine from './MessageAnchorLine'
|
||||
import NarrowLayout from './NarrowLayout'
|
||||
import NewTopicButton from './NewTopicButton'
|
||||
import Prompt from './Prompt'
|
||||
@ -39,7 +40,7 @@ interface MessagesProps {
|
||||
|
||||
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
|
||||
const { t } = useTranslation()
|
||||
const { showTopics, topicPosition, showAssistants } = useSettings()
|
||||
const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||
const dispatch = useAppDispatch()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
@ -225,7 +226,10 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
||||
</InfiniteScroll>
|
||||
<Prompt assistant={assistant} key={assistant.prompt} topic={topic} />
|
||||
</NarrowLayout>
|
||||
<ChatNavigation containerId="messages" />
|
||||
|
||||
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
|
||||
|
||||
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import {
|
||||
setRenderInputMessageAsMarkdown,
|
||||
setShowInputEstimatedTokens,
|
||||
setShowMessageDivider,
|
||||
setMessageNavigation,
|
||||
setThoughtAutoCollapse
|
||||
} from '@renderer/store/settings'
|
||||
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||
@ -76,7 +77,8 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
autoTranslateWithSpace,
|
||||
pasteLongTextThreshold,
|
||||
multiModelMessageStyle,
|
||||
thoughtAutoCollapse
|
||||
thoughtAutoCollapse,
|
||||
messageNavigation
|
||||
} = useSettings()
|
||||
|
||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||
@ -361,6 +363,19 @@ const SettingsTab: FC<Props> = (props) => {
|
||||
</StyledSelect>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('settings.messages.navigation')}</SettingRowTitleSmall>
|
||||
<StyledSelect
|
||||
size="small"
|
||||
value={messageNavigation}
|
||||
onChange={(value) => dispatch(setMessageNavigation(value as 'none' | 'buttons' | 'anchor'))}
|
||||
style={{ width: 135 }}>
|
||||
<Select.Option value="none">{t('settings.messages.navigation.none')}</Select.Option>
|
||||
<Select.Option value="buttons">{t('settings.messages.navigation.buttons')}</Select.Option>
|
||||
<Select.Option value="anchor">{t('settings.messages.navigation.anchor')}</Select.Option>
|
||||
</StyledSelect>
|
||||
</SettingRow>
|
||||
<SettingDivider />
|
||||
<SettingRow>
|
||||
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
||||
<StyledSelect
|
||||
|
||||
@ -40,7 +40,7 @@ const persistedReducer = persistReducer(
|
||||
{
|
||||
key: 'cherry-studio',
|
||||
storage,
|
||||
version: 82,
|
||||
version: 83,
|
||||
blacklist: ['runtime', 'messages'],
|
||||
migrate
|
||||
},
|
||||
|
||||
@ -788,6 +788,10 @@ const migrateConfig = {
|
||||
delete runtimeState.webdavSync
|
||||
}
|
||||
return state
|
||||
},
|
||||
'83': (state: RootState) => {
|
||||
state.settings.messageNavigation = 'buttons'
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -48,6 +48,7 @@ export interface SettingsState {
|
||||
codeStyle: CodeStyleVarious
|
||||
gridColumns: number
|
||||
gridPopoverTrigger: 'hover' | 'click'
|
||||
messageNavigation: 'none' | 'buttons' | 'anchor'
|
||||
// webdav 配置 host, user, pass, path
|
||||
webdavHost: string
|
||||
webdavUser: string
|
||||
@ -122,6 +123,7 @@ const initialState: SettingsState = {
|
||||
codeStyle: 'auto',
|
||||
gridColumns: 2,
|
||||
gridPopoverTrigger: 'hover',
|
||||
messageNavigation: 'none',
|
||||
webdavHost: '',
|
||||
webdavUser: '',
|
||||
webdavPass: '',
|
||||
@ -363,6 +365,9 @@ const settingsSlice = createSlice({
|
||||
},
|
||||
setJoplinUrl: (state, action: PayloadAction<string>) => {
|
||||
state.joplinUrl = action.payload
|
||||
},
|
||||
setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => {
|
||||
state.messageNavigation = action.payload
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -432,7 +437,8 @@ export const {
|
||||
setObsidianApiKey,
|
||||
setObsidianUrl,
|
||||
setJoplinToken,
|
||||
setJoplinUrl
|
||||
setJoplinUrl,
|
||||
setMessageNavigation
|
||||
} = settingsSlice.actions
|
||||
|
||||
export default settingsSlice.reducer
|
||||
|
||||
@ -78,6 +78,10 @@ export type Message = {
|
||||
// MCP Tools
|
||||
mcpTools?: MCPToolResponse[]
|
||||
}
|
||||
// 多模型消息样式
|
||||
multiModelMessageStyle?: 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||
// fold时是否选中
|
||||
foldSelected?: boolean
|
||||
}
|
||||
|
||||
export type Metrics = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user