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."
|
"jsonSaveError": "Failed to save JSON configuration."
|
||||||
},
|
},
|
||||||
"messages.divider": "Show divider between messages",
|
"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_columns": "Message grid display columns",
|
||||||
"messages.grid_popover_trigger": "Grid detail trigger",
|
"messages.grid_popover_trigger": "Grid detail trigger",
|
||||||
"messages.grid_popover_trigger.click": "Click to display",
|
"messages.grid_popover_trigger.click": "Click to display",
|
||||||
|
|||||||
@ -926,6 +926,10 @@
|
|||||||
"jsonSaveError": "JSON設定の保存に失敗しました"
|
"jsonSaveError": "JSON設定の保存に失敗しました"
|
||||||
},
|
},
|
||||||
"messages.divider": "メッセージ間に区切り線を表示",
|
"messages.divider": "メッセージ間に区切り線を表示",
|
||||||
|
"messages.navigation": "メッセージナビゲーション",
|
||||||
|
"messages.navigation.none": "表示しない",
|
||||||
|
"messages.navigation.buttons": "上下ボタン",
|
||||||
|
"messages.navigation.anchor": "会話アンカー",
|
||||||
"messages.grid_columns": "メッセージグリッドの表示列数",
|
"messages.grid_columns": "メッセージグリッドの表示列数",
|
||||||
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
"messages.grid_popover_trigger": "グリッド詳細トリガー",
|
||||||
"messages.grid_popover_trigger.click": "クリックで表示",
|
"messages.grid_popover_trigger.click": "クリックで表示",
|
||||||
|
|||||||
@ -926,6 +926,10 @@
|
|||||||
"jsonSaveError": "Не удалось сохранить конфигурацию JSON"
|
"jsonSaveError": "Не удалось сохранить конфигурацию JSON"
|
||||||
},
|
},
|
||||||
"messages.divider": "Показывать разделитель между сообщениями",
|
"messages.divider": "Показывать разделитель между сообщениями",
|
||||||
|
"messages.navigation": "Навигация сообщений",
|
||||||
|
"messages.navigation.none": "Не показывать",
|
||||||
|
"messages.navigation.buttons": "Кнопки пагинации",
|
||||||
|
"messages.navigation.anchor": "Диалог анкор",
|
||||||
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
"messages.grid_columns": "Количество столбцов сетки сообщений",
|
||||||
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
"messages.grid_popover_trigger": "Триггер для отображения подробной информации в сетке",
|
||||||
"messages.grid_popover_trigger.click": "Нажатие для отображения",
|
"messages.grid_popover_trigger.click": "Нажатие для отображения",
|
||||||
|
|||||||
@ -926,6 +926,10 @@
|
|||||||
"jsonSaveError": "保存JSON配置失败"
|
"jsonSaveError": "保存JSON配置失败"
|
||||||
},
|
},
|
||||||
"messages.divider": "消息分割线",
|
"messages.divider": "消息分割线",
|
||||||
|
"messages.navigation": "对话导航按钮",
|
||||||
|
"messages.navigation.none": "不显示",
|
||||||
|
"messages.navigation.buttons": "上下按钮",
|
||||||
|
"messages.navigation.anchor": "对话锚点",
|
||||||
"messages.grid_columns": "消息网格展示列数",
|
"messages.grid_columns": "消息网格展示列数",
|
||||||
"messages.grid_popover_trigger": "网格详情触发",
|
"messages.grid_popover_trigger": "网格详情触发",
|
||||||
"messages.grid_popover_trigger.click": "点击显示",
|
"messages.grid_popover_trigger.click": "点击显示",
|
||||||
|
|||||||
@ -926,6 +926,10 @@
|
|||||||
"jsonSaveError": "保存JSON配置失敗"
|
"jsonSaveError": "保存JSON配置失敗"
|
||||||
},
|
},
|
||||||
"messages.divider": "訊息間顯示分隔線",
|
"messages.divider": "訊息間顯示分隔線",
|
||||||
|
"messages.navigation": "訊息導航",
|
||||||
|
"messages.navigation.none": "不顯示",
|
||||||
|
"messages.navigation.buttons": "上下按鈕",
|
||||||
|
"messages.navigation.anchor": "對話錨點",
|
||||||
"messages.grid_columns": "訊息網格展示列數",
|
"messages.grid_columns": "訊息網格展示列數",
|
||||||
"messages.grid_popover_trigger": "網格詳細資訊觸發",
|
"messages.grid_popover_trigger": "網格詳細資訊觸發",
|
||||||
"messages.grid_popover_trigger.click": "點選顯示",
|
"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 Scrollbar from '@renderer/components/Scrollbar'
|
||||||
|
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
import { MultiModelMessageStyle } from '@renderer/store/settings'
|
||||||
import type { Message, Topic } from '@renderer/types'
|
import type { Message, Topic } from '@renderer/types'
|
||||||
@ -17,10 +18,12 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
||||||
|
const { editMessage } = useMessageOperations(topic)
|
||||||
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
const { multiModelMessageStyle: multiModelMessageStyleSetting, gridColumns, gridPopoverTrigger } = useSettings()
|
||||||
|
|
||||||
const [multiModelMessageStyle, setMultiModelMessageStyle] =
|
const [multiModelMessageStyle, setMultiModelMessageStyle] = useState<MultiModelMessageStyle>(
|
||||||
useState<MultiModelMessageStyle>(multiModelMessageStyleSetting)
|
messages[0].multiModelMessageStyle || multiModelMessageStyleSetting
|
||||||
|
)
|
||||||
|
|
||||||
const messageLength = messages.length
|
const messageLength = messages.length
|
||||||
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
const [selectedIndex, setSelectedIndex] = useState(messageLength - 1)
|
||||||
@ -33,6 +36,22 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
|||||||
setSelectedIndex(messageLength - 1)
|
setSelectedIndex(messageLength - 1)
|
||||||
}, [messageLength])
|
}, [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(
|
const renderMessage = useCallback(
|
||||||
(message: Message & { index: number }, index: number) => {
|
(message: Message & { index: number }, index: number) => {
|
||||||
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped
|
||||||
@ -49,11 +68,16 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
|||||||
|
|
||||||
const messageWrapper = (
|
const messageWrapper = (
|
||||||
<MessageWrapper
|
<MessageWrapper
|
||||||
|
id={`message-${message.id}`}
|
||||||
$layout={multiModelMessageStyle}
|
$layout={multiModelMessageStyle}
|
||||||
$selected={index === selectedIndex}
|
$selected={index === selectedIndex}
|
||||||
$isGrouped={isGrouped}
|
$isGrouped={isGrouped}
|
||||||
key={message.id}
|
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} />
|
<MessageStream {...messageProps} />
|
||||||
</MessageWrapper>
|
</MessageWrapper>
|
||||||
)
|
)
|
||||||
@ -95,6 +119,7 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<GroupContainer
|
<GroupContainer
|
||||||
|
id={`message-group-${messages[0].askId}`}
|
||||||
$isGrouped={isGrouped}
|
$isGrouped={isGrouped}
|
||||||
$layout={multiModelMessageStyle}
|
$layout={multiModelMessageStyle}
|
||||||
className={classNames([isGrouped && 'group-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
className={classNames([isGrouped && 'group-container', isHorizontal && 'horizontal', isGrid && 'grid'])}>
|
||||||
@ -108,10 +133,14 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
|||||||
{isGrouped && (
|
{isGrouped && (
|
||||||
<MessageGroupMenuBar
|
<MessageGroupMenuBar
|
||||||
multiModelMessageStyle={multiModelMessageStyle}
|
multiModelMessageStyle={multiModelMessageStyle}
|
||||||
setMultiModelMessageStyle={setMultiModelMessageStyle}
|
setMultiModelMessageStyle={(style) => {
|
||||||
|
setMultiModelMessageStyle(style)
|
||||||
|
messages.forEach((message) => {
|
||||||
|
editMessage(message.id, { multiModelMessageStyle: style })
|
||||||
|
})
|
||||||
|
}}
|
||||||
messages={messages}
|
messages={messages}
|
||||||
selectedIndex={selectedIndex}
|
setSelectedMessage={setSelectedMessage}
|
||||||
setSelectedIndex={setSelectedIndex}
|
|
||||||
topic={topic}
|
topic={topic}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -173,15 +202,18 @@ interface MessageWrapperProps {
|
|||||||
|
|
||||||
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
const MessageWrapper = styled(Scrollbar)<MessageWrapperProps>`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: ${(props) => {
|
&.horizontal {
|
||||||
if (props.$layout === 'fold') {
|
display: inline-block;
|
||||||
return props.$selected ? 'block' : 'none'
|
}
|
||||||
|
&.grid {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
&.fold {
|
||||||
|
display: none;
|
||||||
|
&.selected {
|
||||||
|
display: inline-block;
|
||||||
}
|
}
|
||||||
if (props.$layout === 'horizontal') {
|
|
||||||
return 'inline-block'
|
|
||||||
}
|
}
|
||||||
return 'block'
|
|
||||||
}};
|
|
||||||
|
|
||||||
${({ $layout, $isGrouped }) => {
|
${({ $layout, $isGrouped }) => {
|
||||||
if ($layout === 'horizontal' && $isGrouped) {
|
if ($layout === 'horizontal' && $isGrouped) {
|
||||||
|
|||||||
@ -21,8 +21,7 @@ interface Props {
|
|||||||
multiModelMessageStyle: MultiModelMessageStyle
|
multiModelMessageStyle: MultiModelMessageStyle
|
||||||
setMultiModelMessageStyle: (style: MultiModelMessageStyle) => void
|
setMultiModelMessageStyle: (style: MultiModelMessageStyle) => void
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
selectedIndex: number
|
setSelectedMessage: (message: Message) => void
|
||||||
setSelectedIndex: (index: number) => void
|
|
||||||
topic: Topic
|
topic: Topic
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,8 +29,7 @@ const MessageGroupMenuBar: FC<Props> = ({
|
|||||||
multiModelMessageStyle,
|
multiModelMessageStyle,
|
||||||
setMultiModelMessageStyle,
|
setMultiModelMessageStyle,
|
||||||
messages,
|
messages,
|
||||||
selectedIndex,
|
setSelectedMessage,
|
||||||
setSelectedIndex,
|
|
||||||
topic
|
topic
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -77,11 +75,7 @@ const MessageGroupMenuBar: FC<Props> = ({
|
|||||||
))}
|
))}
|
||||||
</LayoutContainer>
|
</LayoutContainer>
|
||||||
{multiModelMessageStyle === 'fold' && (
|
{multiModelMessageStyle === 'fold' && (
|
||||||
<MessageGroupModelList
|
<MessageGroupModelList messages={messages} setSelectedMessage={setSelectedMessage} />
|
||||||
messages={messages}
|
|
||||||
selectedIndex={selectedIndex}
|
|
||||||
setSelectedIndex={setSelectedIndex}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
{multiModelMessageStyle === 'grid' && <MessageGroupSettings />}
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { ArrowsAltOutlined, ShrinkOutlined } from '@ant-design/icons'
|
import { ArrowsAltOutlined, ShrinkOutlined } from '@ant-design/icons'
|
||||||
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
import ModelAvatar from '@renderer/components/Avatar/ModelAvatar'
|
||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
|
||||||
import { Message, Model } from '@renderer/types'
|
import { Message, Model } from '@renderer/types'
|
||||||
import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd'
|
import { Avatar, Segmented as AntdSegmented, Tooltip } from 'antd'
|
||||||
import { FC, useState } from 'react'
|
import { FC, useState } from 'react'
|
||||||
@ -10,13 +9,12 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
interface MessageGroupModelListProps {
|
interface MessageGroupModelListProps {
|
||||||
messages: Message[]
|
messages: Message[]
|
||||||
selectedIndex: number
|
setSelectedMessage: (message: Message) => void
|
||||||
setSelectedIndex: (index: number) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type DisplayMode = 'compact' | 'expanded'
|
type DisplayMode = 'compact' | 'expanded'
|
||||||
|
|
||||||
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selectedIndex, setSelectedIndex }) => {
|
const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, setSelectedMessage }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [displayMode, setDisplayMode] = useState<DisplayMode>('expanded')
|
const [displayMode, setDisplayMode] = useState<DisplayMode>('expanded')
|
||||||
const isCompact = displayMode === 'compact'
|
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}>
|
<Tooltip key={index} title={message.model?.name} placement="top" mouseEnterDelay={0.2}>
|
||||||
<AvatarWrapper
|
<AvatarWrapper
|
||||||
className="avatar-wrapper"
|
className="avatar-wrapper"
|
||||||
isSelected={selectedIndex === index}
|
isSelected={'foldSelected' in message ? message.foldSelected! : index === 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedIndex(index)
|
setSelectedMessage(message)
|
||||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[index].id, false)
|
|
||||||
}}>
|
}}>
|
||||||
<ModelAvatar model={message.model as Model} size={28} />
|
<ModelAvatar model={message.model as Model} size={28} />
|
||||||
</AvatarWrapper>
|
</AvatarWrapper>
|
||||||
@ -56,19 +53,19 @@ const MessageGroupModelList: FC<MessageGroupModelListProps> = ({ messages, selec
|
|||||||
) : (
|
) : (
|
||||||
/* Expanded style display */
|
/* Expanded style display */
|
||||||
<Segmented
|
<Segmented
|
||||||
value={selectedIndex.toString()}
|
value={messages.find((message) => message.foldSelected)?.id || messages[0].id}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setSelectedIndex(Number(value))
|
const message = messages.find((message) => message.id === value) as Message
|
||||||
EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + messages[Number(value)].id, false)
|
setSelectedMessage(message)
|
||||||
}}
|
}}
|
||||||
options={messages.map((message, index) => ({
|
options={messages.map((message) => ({
|
||||||
label: (
|
label: (
|
||||||
<SegmentedLabel>
|
<SegmentedLabel>
|
||||||
<ModelAvatar model={message.model as Model} size={20} />
|
<ModelAvatar model={message.model as Model} size={20} />
|
||||||
<ModelName>{message.model?.name}</ModelName>
|
<ModelName>{message.model?.name}</ModelName>
|
||||||
</SegmentedLabel>
|
</SegmentedLabel>
|
||||||
),
|
),
|
||||||
value: index.toString()
|
value: message.id
|
||||||
}))}
|
}))}
|
||||||
size="small"
|
size="small"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import styled from 'styled-components'
|
|||||||
|
|
||||||
import ChatNavigation from './ChatNavigation'
|
import ChatNavigation from './ChatNavigation'
|
||||||
import MessageGroup from './MessageGroup'
|
import MessageGroup from './MessageGroup'
|
||||||
|
import MessageAnchorLine from './MessageAnchorLine'
|
||||||
import NarrowLayout from './NarrowLayout'
|
import NarrowLayout from './NarrowLayout'
|
||||||
import NewTopicButton from './NewTopicButton'
|
import NewTopicButton from './NewTopicButton'
|
||||||
import Prompt from './Prompt'
|
import Prompt from './Prompt'
|
||||||
@ -39,7 +40,7 @@ interface MessagesProps {
|
|||||||
|
|
||||||
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
|
const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic }) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { showTopics, topicPosition, showAssistants } = useSettings()
|
const { showTopics, topicPosition, showAssistants, messageNavigation } = useSettings()
|
||||||
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
const { updateTopic, addTopic } = useAssistant(assistant.id)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
@ -225,7 +226,10 @@ const Messages: React.FC<MessagesProps> = ({ assistant, topic, setActiveTopic })
|
|||||||
</InfiniteScroll>
|
</InfiniteScroll>
|
||||||
<Prompt assistant={assistant} key={assistant.prompt} topic={topic} />
|
<Prompt assistant={assistant} key={assistant.prompt} topic={topic} />
|
||||||
</NarrowLayout>
|
</NarrowLayout>
|
||||||
<ChatNavigation containerId="messages" />
|
|
||||||
|
{messageNavigation === 'anchor' && <MessageAnchorLine messages={displayMessages} />}
|
||||||
|
|
||||||
|
{messageNavigation === 'buttons' && <ChatNavigation containerId="messages" />}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {
|
|||||||
setRenderInputMessageAsMarkdown,
|
setRenderInputMessageAsMarkdown,
|
||||||
setShowInputEstimatedTokens,
|
setShowInputEstimatedTokens,
|
||||||
setShowMessageDivider,
|
setShowMessageDivider,
|
||||||
|
setMessageNavigation,
|
||||||
setThoughtAutoCollapse
|
setThoughtAutoCollapse
|
||||||
} from '@renderer/store/settings'
|
} from '@renderer/store/settings'
|
||||||
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
import { Assistant, AssistantSettings, CodeStyleVarious, ThemeMode, TranslateLanguageVarious } from '@renderer/types'
|
||||||
@ -76,7 +77,8 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
autoTranslateWithSpace,
|
autoTranslateWithSpace,
|
||||||
pasteLongTextThreshold,
|
pasteLongTextThreshold,
|
||||||
multiModelMessageStyle,
|
multiModelMessageStyle,
|
||||||
thoughtAutoCollapse
|
thoughtAutoCollapse,
|
||||||
|
messageNavigation
|
||||||
} = useSettings()
|
} = useSettings()
|
||||||
|
|
||||||
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
const onUpdateAssistantSettings = (settings: Partial<AssistantSettings>) => {
|
||||||
@ -361,6 +363,19 @@ const SettingsTab: FC<Props> = (props) => {
|
|||||||
</StyledSelect>
|
</StyledSelect>
|
||||||
</SettingRow>
|
</SettingRow>
|
||||||
<SettingDivider />
|
<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>
|
<SettingRow>
|
||||||
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
<SettingRowTitleSmall>{t('message.message.code_style')}</SettingRowTitleSmall>
|
||||||
<StyledSelect
|
<StyledSelect
|
||||||
|
|||||||
@ -40,7 +40,7 @@ const persistedReducer = persistReducer(
|
|||||||
{
|
{
|
||||||
key: 'cherry-studio',
|
key: 'cherry-studio',
|
||||||
storage,
|
storage,
|
||||||
version: 82,
|
version: 83,
|
||||||
blacklist: ['runtime', 'messages'],
|
blacklist: ['runtime', 'messages'],
|
||||||
migrate
|
migrate
|
||||||
},
|
},
|
||||||
|
|||||||
@ -788,6 +788,10 @@ const migrateConfig = {
|
|||||||
delete runtimeState.webdavSync
|
delete runtimeState.webdavSync
|
||||||
}
|
}
|
||||||
return state
|
return state
|
||||||
|
},
|
||||||
|
'83': (state: RootState) => {
|
||||||
|
state.settings.messageNavigation = 'buttons'
|
||||||
|
return state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -48,6 +48,7 @@ export interface SettingsState {
|
|||||||
codeStyle: CodeStyleVarious
|
codeStyle: CodeStyleVarious
|
||||||
gridColumns: number
|
gridColumns: number
|
||||||
gridPopoverTrigger: 'hover' | 'click'
|
gridPopoverTrigger: 'hover' | 'click'
|
||||||
|
messageNavigation: 'none' | 'buttons' | 'anchor'
|
||||||
// webdav 配置 host, user, pass, path
|
// webdav 配置 host, user, pass, path
|
||||||
webdavHost: string
|
webdavHost: string
|
||||||
webdavUser: string
|
webdavUser: string
|
||||||
@ -122,6 +123,7 @@ const initialState: SettingsState = {
|
|||||||
codeStyle: 'auto',
|
codeStyle: 'auto',
|
||||||
gridColumns: 2,
|
gridColumns: 2,
|
||||||
gridPopoverTrigger: 'hover',
|
gridPopoverTrigger: 'hover',
|
||||||
|
messageNavigation: 'none',
|
||||||
webdavHost: '',
|
webdavHost: '',
|
||||||
webdavUser: '',
|
webdavUser: '',
|
||||||
webdavPass: '',
|
webdavPass: '',
|
||||||
@ -363,6 +365,9 @@ const settingsSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setJoplinUrl: (state, action: PayloadAction<string>) => {
|
setJoplinUrl: (state, action: PayloadAction<string>) => {
|
||||||
state.joplinUrl = action.payload
|
state.joplinUrl = action.payload
|
||||||
|
},
|
||||||
|
setMessageNavigation: (state, action: PayloadAction<'none' | 'buttons' | 'anchor'>) => {
|
||||||
|
state.messageNavigation = action.payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -432,7 +437,8 @@ export const {
|
|||||||
setObsidianApiKey,
|
setObsidianApiKey,
|
||||||
setObsidianUrl,
|
setObsidianUrl,
|
||||||
setJoplinToken,
|
setJoplinToken,
|
||||||
setJoplinUrl
|
setJoplinUrl,
|
||||||
|
setMessageNavigation
|
||||||
} = settingsSlice.actions
|
} = settingsSlice.actions
|
||||||
|
|
||||||
export default settingsSlice.reducer
|
export default settingsSlice.reducer
|
||||||
|
|||||||
@ -78,6 +78,10 @@ export type Message = {
|
|||||||
// MCP Tools
|
// MCP Tools
|
||||||
mcpTools?: MCPToolResponse[]
|
mcpTools?: MCPToolResponse[]
|
||||||
}
|
}
|
||||||
|
// 多模型消息样式
|
||||||
|
multiModelMessageStyle?: 'horizontal' | 'vertical' | 'fold' | 'grid'
|
||||||
|
// fold时是否选中
|
||||||
|
foldSelected?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Metrics = {
|
export type Metrics = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user