Skip to content

Commit e6ddfb9

Browse files
committed
feat: Enhance ChatWindow and MessageList components with message selection and scrolling functionality, and update MessageTreeSidebar for improved path visualization and sibling management
1 parent 3d6e855 commit e6ddfb9

4 files changed

Lines changed: 561 additions & 300 deletions

File tree

src/renderer/src/components/pages/chat/ChatWindow.tsx

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import ChatInput, { ChatInputRef } from './ChatInput'
1111
import PageLineageDisplay from '../../common/PageLineageDisplay'
1212
import MessageTreeSidebar from './MessageTreeSidebar'
1313
import { MessageTree } from './messageTree'
14+
import { ChatMessage } from '../../../types/type'
1415

1516
interface ChatWindowProps {
1617
chatId: string
@@ -38,6 +39,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({ chatId }, ref)
3839
const saved = localStorage.getItem('messageTreeWidth')
3940
return saved ? parseInt(saved, 10) : 300
4041
})
42+
const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null)
4143
// 自动提问相关状态
4244
const [autoQuestionEnabled, setAutoQuestionEnabled] = useState(false)
4345
const [autoQuestionMode, setAutoQuestionMode] = useState<'ai' | 'preset'>('ai')
@@ -93,22 +95,59 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({ chatId }, ref)
9395
}, [messageTreeCollapsed])
9496

9597
const handleMessageTreeNodeSelect = useCallback((messageId: string) => {
96-
// 构建到选中消息的路径
97-
const path: string[] = []
98-
let currentMsg = chat?.messages.find((msg) => msg.id === messageId)
98+
if (!chat?.messages) return
9999

100+
// 设置选中的消息ID,用于滚动
101+
setSelectedMessageId(messageId)
102+
103+
const messageMap = new Map<string, ChatMessage>()
104+
chat.messages.forEach((msg) => {
105+
messageMap.set(msg.id, msg)
106+
})
107+
108+
// 构建从根节点到选中节点的路径
109+
const pathToSelected: string[] = []
110+
let currentMsg = messageMap.get(messageId)
100111
while (currentMsg) {
101-
path.unshift(currentMsg.id)
112+
pathToSelected.unshift(currentMsg.id)
102113
if (currentMsg.parentId) {
103-
currentMsg = chat?.messages.find((msg) => msg.id === currentMsg!.parentId)
114+
currentMsg = messageMap.get(currentMsg.parentId)
115+
} else {
116+
break
117+
}
118+
}
119+
120+
// 如果选中的节点在当前路径中,需要保持到叶子节点的完整路径
121+
if (chat.currentPath && chat.currentPath.includes(messageId)) {
122+
// 找到选中节点在当前路径中的位置
123+
const selectedIndex = chat.currentPath.findIndex(id => id === messageId)
124+
if (selectedIndex !== -1) {
125+
// 从根节点到选中节点的路径 + 选中节点之后的原路径
126+
const afterSelected = chat.currentPath.slice(selectedIndex + 1)
127+
const newPath = [...pathToSelected, ...afterSelected]
128+
updateCurrentPath(chatId, newPath)
129+
return
130+
}
131+
}
132+
133+
// 如果不在当前路径中,则需要延续到第一个子节点的路径
134+
let extendedPath = [...pathToSelected]
135+
let lastNode = messageMap.get(messageId)
136+
137+
// 如果该节点有子节点,默认选择第一个子节点并延续路径
138+
while (lastNode && lastNode.children && lastNode.children.length > 0) {
139+
const firstChild = messageMap.get(lastNode.children[0])
140+
if (firstChild) {
141+
extendedPath.push(firstChild.id)
142+
lastNode = firstChild
104143
} else {
105144
break
106145
}
107146
}
108147

109148
// 更新当前路径
110-
updateCurrentPath(chatId, path)
111-
}, [chat?.messages, updateCurrentPath, chatId])
149+
updateCurrentPath(chatId, extendedPath)
150+
}, [chat?.messages, chat?.currentPath, updateCurrentPath, chatId])
112151

113152
const handleMessageTreePathChange = useCallback((path: string[]) => {
114153
// 更新当前路径
@@ -221,6 +260,7 @@ const ChatWindow = forwardRef<ChatWindowRef, ChatWindowProps>(({ chatId }, ref)
221260
streamingContent={chat.streamingMessage?.content}
222261
streamingTimestamp={chat.streamingMessage?.timestamp}
223262
llmConfigs={settings.llmConfigs || []}
263+
selectedMessageId={selectedMessageId}
224264
onRetryMessage={onRetryMessage}
225265
onEditMessage={onEditMessage}
226266
onEditAndResendMessage={onEditAndResendMessage}

src/renderer/src/components/pages/chat/MessageList.tsx

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface MessageListProps {
1313
streamingContent?: string
1414
streamingTimestamp?: number
1515
llmConfigs?: LLMConfig[]
16+
selectedMessageId?: string | null // 新增:选中的消息ID,用于滚动定位
1617
onRetryMessage?: (messageId: string) => void
1718
onEditMessage?: (messageId: string, newContent: string) => void
1819
onEditAndResendMessage?: (messageId: string, newContent: string) => void
@@ -35,6 +36,7 @@ const MessageList = React.memo(function MessageList({
3536
isLoading = false,
3637
streamingContent,
3738
llmConfigs = [],
39+
selectedMessageId,
3840
onRetryMessage,
3941
onEditMessage,
4042
onEditAndResendMessage,
@@ -48,9 +50,11 @@ const MessageList = React.memo(function MessageList({
4850
}: MessageListProps) {
4951
const messagesEndRef = useRef<HTMLDivElement>(null)
5052
const messagesContainerRef = useRef<HTMLDivElement>(null)
53+
const messageRefs = useRef<Map<string, HTMLDivElement>>(new Map())
5154
const prevMessagesLength = useRef<number>(0)
5255
const prevStreamingContent = useRef<string>('')
5356
const prevCurrentPath = useRef<string[]>([])
57+
const prevSelectedMessageId = useRef<string | null>(null)
5458
const isInitialRender = useRef<boolean>(true)
5559

5660
// 控制是否自动滚动到底部
@@ -128,32 +132,51 @@ const MessageList = React.memo(function MessageList({
128132
messagesEndRef.current?.scrollIntoView({ behavior })
129133
}
130134

135+
// 滚动到指定的消息
136+
const scrollToMessage = (messageId: string, behavior: 'smooth' | 'instant' = 'smooth') => {
137+
const messageElement = messageRefs.current.get(messageId)
138+
if (messageElement) {
139+
messageElement.scrollIntoView({ behavior, block: 'center' })
140+
}
141+
}
142+
131143
useEffect(() => {
132144
const currentMessagesLength = messages.length
133145
const currentPathString = JSON.stringify(currentPath)
134146
const prevPathString = JSON.stringify(prevCurrentPath.current)
135147

136148
// 检查路径是否发生变化(排除初次渲染)
137149
const pathChanged = !isInitialRender.current && currentPathString !== prevPathString
150+
151+
// 检查选中消息是否发生变化
152+
const selectedMessageChanged = !isInitialRender.current && selectedMessageId !== prevSelectedMessageId.current
138153

139-
// 只在以下情况下滚动到底部:
140-
// 1. 消息总数增加(有新消息)
141-
// 2. 流式内容发生变化(正在接收AI回复)
142-
// 3. 初次渲染且有消息
143-
// 4. 当前路径发生变化(用户点击消息树切换分支)
144-
const shouldScroll =
154+
// 只在以下情况下滚动:
155+
// 1. 消息总数增加(有新消息)- 滚动到底部
156+
// 2. 流式内容发生变化(正在接收AI回复)- 滚动到底部
157+
// 3. 初次渲染且有消息 - 滚动到底部
158+
// 4. 当前路径发生变化(用户点击消息树切换分支)- 滚动到底部
159+
// 5. 选中消息发生变化(用户点击消息树中的特定消息)- 滚动到选中消息
160+
const shouldScrollToBottom =
145161
currentMessagesLength > prevMessagesLength.current ||
146162
currentStreamingContent !== prevStreamingContent.current ||
147163
(isInitialRender.current && currentMessagesLength > 0) ||
148-
pathChanged
164+
(pathChanged && !selectedMessageChanged)
165+
166+
const shouldScrollToMessage = selectedMessageChanged && selectedMessageId
149167

150168
console.count('shouldScroll');
151169

152170
// 只有在启用自动滚动时才执行滚动
153-
if (shouldScroll && isAutoScrollEnabled) {
154-
// 初次渲染时直接跳转到底部,其他情况平滑滚动
155-
const behavior = isInitialRender.current ? 'instant' : 'smooth'
156-
scrollToBottom(behavior)
171+
if (isAutoScrollEnabled) {
172+
if (shouldScrollToMessage) {
173+
// 滚动到选中的消息
174+
scrollToMessage(selectedMessageId!, 'smooth')
175+
} else if (shouldScrollToBottom) {
176+
// 滚动到底部
177+
const behavior = isInitialRender.current ? 'instant' : 'smooth'
178+
scrollToBottom(behavior)
179+
}
157180
}
158181

159182
// 标记初次渲染已完成
@@ -165,7 +188,8 @@ const MessageList = React.memo(function MessageList({
165188
prevMessagesLength.current = currentMessagesLength
166189
prevStreamingContent.current = currentStreamingContent
167190
prevCurrentPath.current = [...currentPath]
168-
}, [messages.length, currentStreamingContent, currentPath, isAutoScrollEnabled])
191+
prevSelectedMessageId.current = selectedMessageId
192+
}, [messages.length, currentStreamingContent, currentPath, selectedMessageId, isAutoScrollEnabled])
169193

170194
// 处理兄弟分支切换
171195
const handleSiblingBranchSwitch = (messageId: string, direction: 'previous' | 'next') => {
@@ -199,30 +223,40 @@ const MessageList = React.memo(function MessageList({
199223
<div className="messages-container" ref={messagesContainerRef}>
200224
<div className="messages-list">
201225
{displayMessages.map((message, index) => (
202-
<MessageItem
226+
<div
203227
key={message.id}
204-
message={message}
205-
isLoading={isLoading}
206-
isLastMessage={index === displayMessages.length - 1 && !streamingContent}
207-
llmConfigs={llmConfigs}
208-
chatId={chatId} // 传递chatId
209-
// 分支导航props - 所有消息都使用兄弟分支导航
210-
hasChildBranches={messageTree.hasSiblingBranches(message.id)}
211-
branchIndex={messageTree.getCurrentSiblingBranchIndex(message.id)}
212-
branchCount={messageTree.getSiblingBranchCount(message.id)}
213-
onBranchPrevious={(messageId) => handleSiblingBranchSwitch(messageId, 'previous')}
214-
onBranchNext={(messageId) => handleSiblingBranchSwitch(messageId, 'next')}
215-
// 原有的回调
216-
onRetry={onRetryMessage}
217-
onEdit={onEditMessage}
218-
onEditAndResend={onEditAndResendMessage}
219-
onToggleFavorite={onToggleFavorite}
220-
onModelChange={onModelChange}
221-
onDelete={onDeleteMessage}
222-
// 折叠相关
223-
isCollapsed={collapsedMessages.includes(message.id)}
224-
onToggleCollapse={onToggleMessageCollapse}
225-
/>
228+
ref={(el) => {
229+
if (el) {
230+
messageRefs.current.set(message.id, el)
231+
} else {
232+
messageRefs.current.delete(message.id)
233+
}
234+
}}
235+
>
236+
<MessageItem
237+
message={message}
238+
isLoading={isLoading}
239+
isLastMessage={index === displayMessages.length - 1 && !streamingContent}
240+
llmConfigs={llmConfigs}
241+
chatId={chatId} // 传递chatId
242+
// 分支导航props - 所有消息都使用兄弟分支导航
243+
hasChildBranches={messageTree.hasSiblingBranches(message.id)}
244+
branchIndex={messageTree.getCurrentSiblingBranchIndex(message.id)}
245+
branchCount={messageTree.getSiblingBranchCount(message.id)}
246+
onBranchPrevious={(messageId) => handleSiblingBranchSwitch(messageId, 'previous')}
247+
onBranchNext={(messageId) => handleSiblingBranchSwitch(messageId, 'next')}
248+
// 原有的回调
249+
onRetry={onRetryMessage}
250+
onEdit={onEditMessage}
251+
onEditAndResend={onEditAndResendMessage}
252+
onToggleFavorite={onToggleFavorite}
253+
onModelChange={onModelChange}
254+
onDelete={onDeleteMessage}
255+
// 折叠相关
256+
isCollapsed={collapsedMessages.includes(message.id)}
257+
onToggleCollapse={onToggleMessageCollapse}
258+
/>
259+
</div>
226260
))}
227261

228262
<div ref={messagesEndRef} />

0 commit comments

Comments
 (0)