diff --git a/package.json b/package.json index d6ba6490..2b743267 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,10 @@ "@llm-tools/embedjs-loader-xml": "^0.1.28", "@llm-tools/embedjs-openai": "^0.1.28", "@modelcontextprotocol/sdk": "patch:@modelcontextprotocol/sdk@npm%3A1.6.1#~/.yarn/patches/@modelcontextprotocol-sdk-npm-1.6.1-b46313efe7.patch", + "@notionhq/client": "^2.2.15", + "@tryfabric/martian": "^1.2.4", + "@types/react-infinite-scroll-component": "^5.0.0", + "@xyflow/react": "^12.4.4", "adm-zip": "^0.5.16", "docx": "^9.0.2", "electron-log": "^5.1.5", diff --git a/src/renderer/src/i18n/locales/en-us.json b/src/renderer/src/i18n/locales/en-us.json index e35c2a3e..6271db37 100644 --- a/src/renderer/src/i18n/locales/en-us.json +++ b/src/renderer/src/i18n/locales/en-us.json @@ -200,6 +200,25 @@ "topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic", "topics.title": "Topics", "topics.unpinned": "Unpinned Topics", + "topics.new": "New Topic", + "translate": "Translate", + "navigation": { + "prev": "Previous Message", + "next": "Next Message", + "first": "Already at the first message", + "last": "Already at the last message", + "history": "Chat History" + }, + "history": { + "title": "Chat History", + "coming_soon": "Chat workflow diagram coming soon", + "no_messages": "No Messages Found", + "start_conversation": "Start a conversation to see the chat flow diagram", + "user_node": "User", + "assistant_node": "Assistant", + "view_full_content": "View Full Content", + "click_to_navigate": "Click to navigate to the message" + } "translate": "Translate" }, "code_block": { diff --git a/src/renderer/src/i18n/locales/ja-jp.json b/src/renderer/src/i18n/locales/ja-jp.json index 3d6bab82..6dc0080b 100644 --- a/src/renderer/src/i18n/locales/ja-jp.json +++ b/src/renderer/src/i18n/locales/ja-jp.json @@ -200,6 +200,25 @@ "topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供", "topics.title": "トピック", "topics.unpinned": "固定解除", + "topics.new": "新しいトピック", + "translate": "翻訳", + "navigation": { + "prev": "前のメッセージ", + "next": "次のメッセージ", + "first": "最初のメッセージです", + "last": "最後のメッセージです", + "history": "チャット履歴" + }, + "history": { + "title": "チャット履歴", + "coming_soon": "チャットワークフロー図がすぐに登場します", + "no_messages": "メッセージが見つかりませんでした", + "start_conversation": "チャットを開始してチャットワークフロー図を確認してください", + "user_node": "ユーザー", + "assistant_node": "アシスタント", + "view_full_content": "完全な内容を表示", + "click_to_navigate": "メッセージに移動" + } "translate": "翻訳" }, "code_block": { diff --git a/src/renderer/src/i18n/locales/ru-ru.json b/src/renderer/src/i18n/locales/ru-ru.json index d3c854c4..a53c2e73 100644 --- a/src/renderer/src/i18n/locales/ru-ru.json +++ b/src/renderer/src/i18n/locales/ru-ru.json @@ -200,6 +200,25 @@ "topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы", "topics.title": "Топики", "topics.unpinned": "Открепленные темы", + "topics.new": "Новый топик", + "translate": "Перевести", + "navigation": { + "prev": "Предыдущее сообщение", + "next": "Следующее сообщение", + "first": "Уже первое сообщение", + "last": "Уже последнее сообщение", + "history": "История чата" + }, + "history": { + "title": "История чата", + "coming_soon": "График работы чата скоро появится", + "no_messages": "Сообщения не найдены", + "start_conversation": "Начните диалог, чтобы просмотреть график работы чата", + "user_node": "Пользователь", + "assistant_node": "Ассистент", + "view_full_content": "Показать полное содержимое", + "click_to_navigate": "Перейти к сообщению" + } "translate": "Перевести" }, "code_block": { diff --git a/src/renderer/src/i18n/locales/zh-cn.json b/src/renderer/src/i18n/locales/zh-cn.json index 2442ad06..94b03be9 100644 --- a/src/renderer/src/i18n/locales/zh-cn.json +++ b/src/renderer/src/i18n/locales/zh-cn.json @@ -200,6 +200,25 @@ "topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词", "topics.title": "话题", "topics.unpinned": "取消固定", + "topics.new": "开始新对话", + "translate": "翻译", + "navigation": { + "prev": "上一条消息", + "next": "下一条消息", + "first": "已经是第一条消息", + "last": "已经是最后一条消息", + "history": "聊天历史" + }, + "history": { + "title": "聊天历史", + "coming_soon": "聊天工作流图表即将上线", + "no_messages": "没有找到消息", + "start_conversation": "开始对话以查看聊天流程图", + "user_node": "用户", + "assistant_node": "助手", + "view_full_content": "查看完整内容", + "click_to_navigate": "点击跳转到对应消息" + } "translate": "翻译" }, "code_block": { diff --git a/src/renderer/src/i18n/locales/zh-tw.json b/src/renderer/src/i18n/locales/zh-tw.json index 3c13cc69..489c0896 100644 --- a/src/renderer/src/i18n/locales/zh-tw.json +++ b/src/renderer/src/i18n/locales/zh-tw.json @@ -200,6 +200,25 @@ "topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞", "topics.title": "話題", "topics.unpinned": "取消固定", + "topics.new": "開始新對話", + "translate": "翻譯", + "navigation": { + "prev": "上一條訊息", + "next": "下一條訊息", + "first": "已經是第一條訊息", + "last": "已經是最後一條訊息", + "history": "聊天歷史" + }, + "history": { + "title": "聊天歷史", + "coming_soon": "聊天工作流圖表即將上線", + "no_messages": "沒有找到訊息", + "start_conversation": "開始對話以查看聊天流程圖", + "user_node": "用戶", + "assistant_node": "助手", + "view_full_content": "查看完整內容", + "click_to_navigate": "點擊跳轉到對應訊息" + } "translate": "翻譯" }, "code_block": { diff --git a/src/renderer/src/pages/home/Messages/ChatFlowHistory.tsx b/src/renderer/src/pages/home/Messages/ChatFlowHistory.tsx new file mode 100644 index 00000000..df788ca8 --- /dev/null +++ b/src/renderer/src/pages/home/Messages/ChatFlowHistory.tsx @@ -0,0 +1,614 @@ +import '@xyflow/react/dist/style.css' + +import { RobotOutlined, UserOutlined } from '@ant-design/icons' +import ModelAvatar from '@renderer/components/Avatar/ModelAvatar' +import { getModelLogo } from '@renderer/config/models' +import { useSettings } from '@renderer/hooks/useSettings' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' +import { RootState } from '@renderer/store' +import { selectTopicMessages } from '@renderer/store/messages' +import { Model } from '@renderer/types' +import { Controls, Handle, MiniMap, ReactFlow, ReactFlowProvider } from '@xyflow/react' +import { Edge, Node, NodeTypes, Position, useEdgesState, useNodesState } from '@xyflow/react' +import { Avatar, Spin, Tooltip } from 'antd' +import { isEqual } from 'lodash' +import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' +import styled from 'styled-components' + +// 定义Tooltip相关样式组件 +const TooltipContent = styled.div` + max-width: 300px; +` + +const TooltipTitle = styled.div` + font-weight: bold; + margin-bottom: 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + padding-bottom: 4px; +` + +const TooltipBody = styled.div` + max-height: 200px; + overflow-y: auto; + margin-bottom: 8px; + white-space: pre-wrap; +` + +const TooltipFooter = styled.div` + font-size: 12px; + color: rgba(255, 255, 255, 0.7); + font-style: italic; +` + +// 自定义节点组件 +const CustomNode: FC<{ data: any }> = ({ data }) => { + const { t } = useTranslation() + const nodeType = data.type + let borderColor = 'var(--color-border)' + let title = '' + let backgroundColor = 'var(--bg-color)' + let gradientColor = 'rgba(0, 0, 0, 0.03)' + let avatar: JSX.Element | null = null + + // 根据消息类型设置不同的样式和图标 + if (nodeType === 'user') { + borderColor = 'var(--color-icon)' + backgroundColor = 'rgba(var(--color-info-rgb), 0.03)' + gradientColor = 'rgba(var(--color-info-rgb), 0.08)' + title = data.userName || t('chat.history.user_node') + + // 用户头像 + if (data.userAvatar) { + avatar = + } else { + avatar = } style={{ backgroundColor: 'var(--color-info)' }} /> + } + } else if (nodeType === 'assistant') { + borderColor = 'var(--color-primary)' + backgroundColor = 'rgba(var(--color-primary-rgb), 0.03)' + gradientColor = 'rgba(var(--color-primary-rgb), 0.08)' + title = `${data.model || t('chat.history.assistant_node')}` + + // 模型头像 + if (data.modelInfo) { + avatar = + } else if (data.modelId) { + const modelLogo = getModelLogo(data.modelId) + avatar = ( + : undefined} + style={{ backgroundColor: 'var(--color-primary)' }} + /> + ) + } else { + avatar = } style={{ backgroundColor: 'var(--color-primary)' }} /> + } + } + + // 处理节点点击事件,滚动到对应消息 + const handleNodeClick = () => { + if (data.messageId) { + // 创建一个自定义事件来定位消息并切换标签 + const customEvent = new CustomEvent('flow-navigate-to-message', { + detail: { + messageId: data.messageId, + modelId: data.modelId, + modelName: data.model, + nodeType: nodeType + }, + bubbles: true + }) + + // 让监听器处理标签切换 + document.dispatchEvent(customEvent) + + setTimeout(() => { + EventEmitter.emit(EVENT_NAMES.LOCATE_MESSAGE + ':' + data.messageId) + }, 250) + } + } + + // 隐藏连接点的通用样式 + const handleStyle = { + opacity: 0, + width: '12px', + height: '12px', + background: 'transparent', + border: 'none' + } + + return ( + + {title} + {data.content} + {t('chat.history.click_to_navigate')} + + } + placement="top" + color="rgba(0, 0, 0, 0.85)" + mouseEnterDelay={0.3} + mouseLeaveDelay={0.1} + destroyTooltipOnHide> + + + + + + {avatar} + {title} + + {data.content} + + + + + + ) +} + +// 创建自定义节点类型 +const nodeTypes: NodeTypes = { custom: CustomNode } + +interface ChatFlowHistoryProps { + conversationId?: string +} + +// 定义节点和边的类型 +type FlowNode = Node +type FlowEdge = Edge + +// 统一的边样式 +const commonEdgeStyle = { + stroke: 'var(--color-border)', + strokeDasharray: '4,4', + strokeWidth: 2 +} + +// 统一的边配置 +const defaultEdgeOptions = { + animated: true, + style: commonEdgeStyle, + type: 'step', + markerEnd: undefined, + zIndex: 5 +} + +const ChatFlowHistory: FC = ({ conversationId }) => { + const { t } = useTranslation() + const [nodes, setNodes, onNodesChange] = useNodesState([]) + const [edges, setEdges, onEdgesChange] = useEdgesState([]) + const [loading, setLoading] = useState(true) + const { userName } = useSettings() + + const topicId = conversationId + + // 只在消息实际内容变化时更新,而不是属性变化(如foldSelected) + const messages = useSelector( + (state: RootState) => selectTopicMessages(state, topicId || ''), + (prev, next) => { + // 只比较消息的关键属性,忽略展示相关的属性(如foldSelected) + if (prev.length !== next.length) return false + + // 比较每条消息的内容和关键属性,忽略UI状态相关属性 + return prev.every((prevMsg, index) => { + const nextMsg = next[index] + return ( + prevMsg.id === nextMsg.id && + prevMsg.content === nextMsg.content && + prevMsg.role === nextMsg.role && + prevMsg.createdAt === nextMsg.createdAt && + prevMsg.askId === nextMsg.askId && + isEqual(prevMsg.model, nextMsg.model) + ) + }) + } + ) + + // 获取用户头像 + const userAvatar = useSelector((state: RootState) => state.runtime.avatar) + + // 消息过滤 + const { userMessages, assistantMessages } = useMemo(() => { + const userMsgs = messages.filter((msg) => msg.role === 'user') + const assistantMsgs = messages.filter((msg) => msg.role === 'assistant') + return { userMessages: userMsgs, assistantMessages: assistantMsgs } + }, [messages]) + + const buildConversationFlowData = useCallback(() => { + if (!topicId || !messages.length) return { nodes: [], edges: [] } + + // 创建节点和边 + const flowNodes: FlowNode[] = [] + const flowEdges: FlowEdge[] = [] + + // 布局参数 + const verticalGap = 200 + const horizontalGap = 350 + const baseX = 150 + + // 如果没有任何消息可以显示,返回空结果 + if (userMessages.length === 0 && assistantMessages.length === 0) { + return { nodes: [], edges: [] } + } + + // 为所有用户消息创建节点 + userMessages.forEach((message, index) => { + const nodeId = `user-${message.id}` + const yPosition = index * verticalGap * 2 + + // 获取用户名 + const userNameValue = userName || t('chat.history.user_node') + + // 获取用户头像 + const msgUserAvatar = userAvatar || null + + flowNodes.push({ + id: nodeId, + type: 'custom', + data: { + userName: userNameValue, + content: message.content, + type: 'user', + messageId: message.id, + userAvatar: msgUserAvatar + }, + position: { x: baseX, y: yPosition }, + sourcePosition: Position.Bottom, + targetPosition: Position.Top + }) + + // 找到用户消息之后的助手回复 + const userMsgTime = new Date(message.createdAt).getTime() + const relatedAssistantMsgs = assistantMessages.filter((aMsg) => { + const aMsgTime = new Date(aMsg.createdAt).getTime() + return ( + aMsgTime > userMsgTime && + (index === userMessages.length - 1 || aMsgTime < new Date(userMessages[index + 1].createdAt).getTime()) + ) + }) + + // 为相关的助手消息创建节点 + relatedAssistantMsgs.forEach((aMsg, aIndex) => { + const assistantNodeId = `assistant-${aMsg.id}` + const isMultipleResponses = relatedAssistantMsgs.length > 1 + const assistantX = baseX + (isMultipleResponses ? horizontalGap * aIndex : 0) + const assistantY = yPosition + verticalGap + + // 根据位置确定连接点位置 + let sourcePos = Position.Bottom // 默认向下输出 + let targetPos = Position.Top // 默认从上方输入 + + // 横向排列多个助手消息时调整连接点 + // 注意:现在所有助手节点都直接连接到用户节点,而不是相互连接 + if (isMultipleResponses) { + // 所有助手节点都使用顶部作为输入点(从用户节点) + targetPos = Position.Top + + // 所有助手节点都使用底部作为输出点(到下一个用户节点) + sourcePos = Position.Bottom + } + + const aMsgAny = aMsg as any + + // 获取模型名称 + const modelName = (aMsgAny.model && aMsgAny.model.name) || t('chat.history.assistant_node') + + // 获取模型ID + const modelId = (aMsgAny.model && aMsgAny.model.id) || '' + + // 完整的模型信息 + const modelInfo = aMsgAny.model as Model | undefined + + flowNodes.push({ + id: assistantNodeId, + type: 'custom', + data: { + model: modelName, + content: aMsg.content, + type: 'assistant', + messageId: aMsg.id, + modelId: modelId, + modelInfo + }, + position: { x: assistantX, y: assistantY }, + sourcePosition: sourcePos, + targetPosition: targetPos + }) + + // 连接消息 - 将每个助手节点直接连接到用户节点 + if (aIndex === 0) { + // 连接用户消息到第一个助手回复 + flowEdges.push({ + id: `edge-${nodeId}-to-${assistantNodeId}`, + source: nodeId, + target: assistantNodeId + }) + } else { + // 直接连接用户消息到所有其他助手回复 + flowEdges.push({ + id: `edge-${nodeId}-to-${assistantNodeId}`, + source: nodeId, + target: assistantNodeId + }) + } + }) + + // 连接相邻的用户消息 + if (index > 0) { + const prevUserNodeId = `user-${userMessages[index - 1].id}` + const prevUserTime = new Date(userMessages[index - 1].createdAt).getTime() + + // 查找前一个用户消息的所有助手回复 + const prevAssistantMsgs = assistantMessages.filter((aMsg) => { + const aMsgTime = new Date(aMsg.createdAt).getTime() + return aMsgTime > prevUserTime && aMsgTime < userMsgTime + }) + + if (prevAssistantMsgs.length > 0) { + // 所有前一个用户的助手消息都连接到当前用户消息 + prevAssistantMsgs.forEach((aMsg) => { + const assistantId = `assistant-${aMsg.id}` + flowEdges.push({ + id: `edge-${assistantId}-to-${nodeId}`, + source: assistantId, + target: nodeId + }) + }) + } else { + // 如果没有助手消息,直接连接两个用户消息 + flowEdges.push({ + id: `edge-${prevUserNodeId}-to-${nodeId}`, + source: prevUserNodeId, + target: nodeId + }) + } + } + }) + + // 处理孤立的助手消息(没有对应的用户消息) + const orphanAssistantMsgs = assistantMessages.filter( + (aMsg) => !flowNodes.some((node) => node.id === `assistant-${aMsg.id}`) + ) + + if (orphanAssistantMsgs.length > 0) { + // 在图表顶部添加这些孤立消息 + const startY = flowNodes.length > 0 ? Math.min(...flowNodes.map((node) => node.position.y)) - verticalGap * 2 : 0 + + orphanAssistantMsgs.forEach((aMsg, index) => { + const assistantNodeId = `orphan-assistant-${aMsg.id}` + + // 获取模型数据 + const aMsgAny = aMsg as any + + // 获取模型名称 + const modelName = (aMsgAny.model && aMsgAny.model.name) || t('chat.history.assistant_node') + + // 获取模型ID + const modelId = (aMsgAny.model && aMsgAny.model.id) || '' + + // 完整的模型信息 + const modelInfo = aMsgAny.model as Model | undefined + + flowNodes.push({ + id: assistantNodeId, + type: 'custom', + data: { + model: modelName, + content: aMsg.content, + type: 'assistant', + messageId: aMsg.id, + modelId: modelId, + modelInfo + }, + position: { x: baseX, y: startY - index * verticalGap }, + sourcePosition: Position.Bottom, + targetPosition: Position.Top + }) + + // 连接相邻的孤立消息 + if (index > 0) { + const prevNodeId = `orphan-assistant-${orphanAssistantMsgs[index - 1].id}` + flowEdges.push({ + id: `edge-${prevNodeId}-to-${assistantNodeId}`, + source: prevNodeId, + target: assistantNodeId + }) + } + }) + } + + return { nodes: flowNodes, edges: flowEdges } + }, [topicId, messages, userMessages, assistantMessages, t]) + + useEffect(() => { + setLoading(true) + setTimeout(() => { + const { nodes: flowNodes, edges: flowEdges } = buildConversationFlowData() + setNodes([...flowNodes]) + setEdges([...flowEdges]) + setLoading(false) + }, 500) + }, [buildConversationFlowData, setNodes, setEdges]) + + return ( + + {loading ? ( + + + + ) : nodes.length > 0 ? ( + +
+ + + (node.data.type === 'user' ? 'var(--color-info)' : 'var(--color-primary)')} + /> + +
+
+ ) : ( + + {t('chat.history.no_messages')} + + )} +
+ ) +} + +// 样式组件定义 +const FlowContainer = styled.div` + width: 100%; + height: 100%; + min-height: 500px; +` + +const LoadingContainer = styled.div` + width: 100%; + height: 100%; + min-height: 500px; + display: flex; + justify-content: center; + align-items: center; +` + +const EmptyContainer = styled.div` + width: 100%; + height: 100%; + min-height: 500px; + display: flex; + justify-content: center; + align-items: center; + color: var(--color-text-secondary); +` + +const EmptyText = styled.div` + font-size: 16px; + margin-bottom: 8px; + font-weight: bold; +` + +const CustomNodeContainer = styled.div` + padding: 12px; + border-radius: 10px; + border: 2px solid; + width: 280px; + height: 120px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease-in-out; + overflow: hidden; + display: flex; + flex-direction: column; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + + &:hover { + transform: translateY(-2px); + box-shadow: + 0 6px 10px rgba(0, 0, 0, 0.1), + 0 0 0 2px ${(props) => props.style?.borderColor || 'var(--color-border)'}80 !important; + filter: brightness(1.02); + } + + /* 添加点击动画效果 */ + &:active { + transform: scale(0.98); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: all 0.1s ease; + } +` + +const NodeHeader = styled.div` + font-weight: bold; + margin-bottom: 8px; + padding-bottom: 8px; + border-bottom: 1px solid rgba(0, 0, 0, 0.2); + color: var(--color-text); + display: flex; + align-items: center; + min-height: 32px; +` + +const NodeAvatar = styled.span` + margin-right: 10px; + display: flex; + align-items: center; + + .ant-avatar { + display: flex; + align-items: center; + justify-content: center; + border: 1px solid rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } +` + +const NodeTitle = styled.span` + flex: 1; + font-size: 16px; + font-weight: bold; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +` + +const NodeContent = styled.div` + margin: 2px 0; + color: var(--color-text); + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + line-height: 1.5; + word-break: break-word; + font-size: 14px; + padding: 3px; +` + +// 确保组件使用React.memo包装以减少不必要的重渲染 +export default memo(ChatFlowHistory, (prevProps, nextProps) => { + return prevProps.conversationId === nextProps.conversationId +}) diff --git a/src/renderer/src/pages/home/Messages/ChatNavigation.tsx b/src/renderer/src/pages/home/Messages/ChatNavigation.tsx index 2d8bbc41..3f9c0c96 100644 --- a/src/renderer/src/pages/home/Messages/ChatNavigation.tsx +++ b/src/renderer/src/pages/home/Messages/ChatNavigation.tsx @@ -1,10 +1,15 @@ -import { DownOutlined, UpOutlined } from '@ant-design/icons' +import { DownOutlined, HistoryOutlined, UpOutlined } from '@ant-design/icons' import { useSettings } from '@renderer/hooks/useSettings' -import { Button, Tooltip } from 'antd' +import { RootState } from '@renderer/store' +import { selectCurrentTopicId } from '@renderer/store/messages' +import { Button, Drawer, Tooltip } from 'antd' import { FC, useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { useSelector } from 'react-redux' import styled from 'styled-components' +import ChatFlowHistory from './ChatFlowHistory' + interface ChatNavigationProps { containerId: string } @@ -14,6 +19,8 @@ const ChatNavigation: FC = ({ containerId }) => { const [isVisible, setIsVisible] = useState(false) const [isNearButtons, setIsNearButtons] = useState(false) const [hideTimer, setHideTimer] = useState(null) + const [showChatHistory, setShowChatHistory] = useState(false) + const currentTopicId = useSelector((state: RootState) => selectCurrentTopicId(state)) const lastMoveTime = useRef(0) const { topicPosition, showTopics } = useSettings() const showRightTopics = topicPosition === 'right' && showTopics @@ -59,6 +66,15 @@ const ChatNavigation: FC = ({ containerId }) => { setHideTimer(timer) }, []) + const handleChatHistoryClick = () => { + setShowChatHistory(true) + resetHideTimer() + } + + const handleDrawerClose = () => { + setShowChatHistory(false) + } + const findUserMessages = () => { const container = document.getElementById(containerId) if (!container) return [] @@ -259,31 +275,58 @@ const ChatNavigation: FC = ({ containerId }) => { ]) return ( - - - - } - onClick={handlePrevMessage} - aria-label={t('chat.navigation.prev')} - /> - - - - } - onClick={handleNextMessage} - aria-label={t('chat.navigation.next')} - /> - - - + <> + + + + } + onClick={handlePrevMessage} + aria-label={t('chat.navigation.prev')} + /> + + + + } + onClick={handleNextMessage} + aria-label={t('chat.navigation.next')} + /> + + + + } + onClick={handleChatHistoryClick} + aria-label={t('chat.navigation.history')} + /> + + + + + + + + ) } diff --git a/src/renderer/src/pages/home/Messages/MessageGroup.tsx b/src/renderer/src/pages/home/Messages/MessageGroup.tsx index 89586b9c..b254fe9e 100644 --- a/src/renderer/src/pages/home/Messages/MessageGroup.tsx +++ b/src/renderer/src/pages/home/Messages/MessageGroup.tsx @@ -1,6 +1,7 @@ import Scrollbar from '@renderer/components/Scrollbar' import { useMessageOperations } from '@renderer/hooks/useMessageOperations' import { useSettings } from '@renderer/hooks/useSettings' +import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService' import { MultiModelMessageStyle } from '@renderer/store/settings' import type { Message, Topic } from '@renderer/types' import { classNames } from '@renderer/utils' @@ -36,6 +37,38 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => { setSelectedIndex(messageLength - 1) }, [messageLength]) + // 添加对流程图节点点击事件的监听 + useEffect(() => { + // 只在组件挂载和消息数组变化时添加监听器 + if (!isGrouped || messageLength <= 1) return + + const handleFlowNavigate = (event: CustomEvent) => { + const { messageId } = event.detail + + // 查找对应的消息在当前消息组中的索引 + const targetIndex = messages.findIndex((msg) => msg.id === messageId) + + // 如果找到消息且不是当前选中的索引,则切换标签 + if (targetIndex !== -1 && targetIndex !== selectedIndex) { + setSelectedIndex(targetIndex) + + // 使用setSelectedMessage函数来切换标签,这是处理foldSelected的关键 + const targetMessage = messages[targetIndex] + if (targetMessage) { + setSelectedMessage(targetMessage) + } + } + } + + // 添加事件监听器 + document.addEventListener('flow-navigate-to-message', handleFlowNavigate as EventListener) + + // 清理函数 + return () => { + document.removeEventListener('flow-navigate-to-message', handleFlowNavigate as EventListener) + } + }, [messages, selectedIndex, isGrouped, messageLength]) + const setSelectedMessage = useCallback( (message: Message) => { messages.forEach(async (m) => { @@ -47,11 +80,47 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => { if (messageElement) { messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' }) } - }, 100) + }, 200) }, [editMessage, messages] ) + // 添加对LOCATE_MESSAGE事件的监听 + useEffect(() => { + // 为每个消息注册一个定位事件监听器 + const eventHandlers: { [key: string]: () => void } = {} + + messages.forEach((message) => { + const eventName = EVENT_NAMES.LOCATE_MESSAGE + ':' + message.id + const handler = () => { + // 检查消息是否处于可见状态 + const element = document.getElementById(`message-${message.id}`) + if (element) { + const display = window.getComputedStyle(element).display + + if (display === 'none') { + // 如果消息隐藏,先切换标签 + setSelectedMessage(message) + } else { + // 直接滚动 + element.scrollIntoView({ behavior: 'smooth', block: 'start' }) + } + } + } + + eventHandlers[eventName] = handler + EventEmitter.on(eventName, handler) + }) + + // 清理函数 + return () => { + // 移除所有事件监听器 + Object.entries(eventHandlers).forEach(([eventName, handler]) => { + EventEmitter.off(eventName, handler) + }) + } + }, [messages, setSelectedMessage]) + const renderMessage = useCallback( (message: Message & { index: number }, index: number) => { const isGridGroupMessage = isGrid && message.role === 'assistant' && isGrouped diff --git a/yarn.lock b/yarn.lock index dba5bedd..2d9f3ed3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2798,6 +2798,71 @@ __metadata: languageName: node linkType: hard +"@types/command-line-args@npm:^5.2.3": + version: 5.2.3 + resolution: "@types/command-line-args@npm:5.2.3" + checksum: 10c0/3a9bc58fd26e546391f6369dd28c03d59349dc4ac39eada1a5c39cc3578e02e4aac222615170e0db79b198ffba2af84fdbdda46e08c6edc4da42bc17ea85200f + languageName: node + linkType: hard + +"@types/command-line-usage@npm:^5.0.4": + version: 5.0.4 + resolution: "@types/command-line-usage@npm:5.0.4" + checksum: 10c0/67840ebf4bcfee200c07d978669ad596fe2adc350fd5c19d44ec2248623575d96ec917f513d1d59453f8f57e879133861a4cc41c20045c07f6c959f1fcaac7ad + languageName: node + linkType: hard + +"@types/d3-color@npm:*": + version: 3.1.3 + resolution: "@types/d3-color@npm:3.1.3" + checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae + languageName: node + linkType: hard + +"@types/d3-drag@npm:^3.0.7": + version: 3.0.7 + resolution: "@types/d3-drag@npm:3.0.7" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/65e29fa32a87c72d26c44b5e2df3bf15af21cd128386bcc05bcacca255927c0397d0cd7e6062aed5f0abd623490544a9d061c195f5ed9f018fe0b698d99c079d + languageName: node + linkType: hard + +"@types/d3-interpolate@npm:*": + version: 3.0.4 + resolution: "@types/d3-interpolate@npm:3.0.4" + dependencies: + "@types/d3-color": "npm:*" + checksum: 10c0/066ebb8da570b518dd332df6b12ae3b1eaa0a7f4f0c702e3c57f812cf529cc3500ec2aac8dc094f31897790346c6b1ebd8cd7a077176727f4860c2b181a65ca4 + languageName: node + linkType: hard + +"@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10": + version: 3.0.11 + resolution: "@types/d3-selection@npm:3.0.11" + checksum: 10c0/0c512956c7503ff5def4bb32e0c568cc757b9a2cc400a104fc0f4cfe5e56d83ebde2a97821b6f2cb26a7148079d3b86a2f28e11d68324ed311cf35c2ed980d1d + languageName: node + linkType: hard + +"@types/d3-transition@npm:^3.0.8": + version: 3.0.9 + resolution: "@types/d3-transition@npm:3.0.9" + dependencies: + "@types/d3-selection": "npm:*" + checksum: 10c0/4f68b9df7ac745b3491216c54203cbbfa0f117ae4c60e2609cdef2db963582152035407fdff995b10ee383bae2f05b7743493f48e1b8e46df54faa836a8fb7b5 + languageName: node + linkType: hard + +"@types/d3-zoom@npm:^3.0.8": + version: 3.0.8 + resolution: "@types/d3-zoom@npm:3.0.8" + dependencies: + "@types/d3-interpolate": "npm:*" + "@types/d3-selection": "npm:*" + checksum: 10c0/1dbdbcafddcae12efb5beb6948546963f29599e18bc7f2a91fb69cc617c2299a65354f2d47e282dfb86fec0968406cd4fb7f76ba2d2fb67baa8e8d146eb4a547 + languageName: node + linkType: hard + "@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.6": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" @@ -3306,6 +3371,35 @@ __metadata: languageName: node linkType: hard +"@xyflow/react@npm:^12.4.4": + version: 12.4.4 + resolution: "@xyflow/react@npm:12.4.4" + dependencies: + "@xyflow/system": "npm:0.0.52" + classcat: "npm:^5.0.3" + zustand: "npm:^4.4.0" + peerDependencies: + react: ">=17" + react-dom: ">=17" + checksum: 10c0/abce710e98a414def5f5cb847831ad49e25e3de5ebeaee4e4f9c8b651c1a507b1d61b8e11b8f1f519e9f7500f37d07218fbbd23d797c137e1e3f5515dc759c98 + languageName: node + linkType: hard + +"@xyflow/system@npm:0.0.52": + version: 0.0.52 + resolution: "@xyflow/system@npm:0.0.52" + dependencies: + "@types/d3-drag": "npm:^3.0.7" + "@types/d3-selection": "npm:^3.0.10" + "@types/d3-transition": "npm:^3.0.8" + "@types/d3-zoom": "npm:^3.0.8" + d3-drag: "npm:^3.0.0" + d3-selection: "npm:^3.0.0" + d3-zoom: "npm:^3.0.0" + checksum: 10c0/34822192065f4d1358c781882b5f7e1eba6d6953aa741e480de704080a7d0609823b124d90fec17a747226150e980c12d9cd145642aa795ff94054c6cc67dbaa + languageName: node + linkType: hard + "CherryStudio@workspace:.": version: 0.0.0-use.local resolution: "CherryStudio@workspace:." @@ -3355,6 +3449,7 @@ __metadata: "@types/react-infinite-scroll-component": "npm:^5.0.0" "@types/tinycolor2": "npm:^1" "@vitejs/plugin-react": "npm:^4.2.1" + "@xyflow/react": "npm:^12.4.4" adm-zip: "npm:^0.5.16" antd: "npm:^5.22.5" applescript: "npm:^1.0.0" @@ -4592,6 +4687,13 @@ __metadata: languageName: node linkType: hard +"classcat@npm:^5.0.3": + version: 5.0.5 + resolution: "classcat@npm:5.0.5" + checksum: 10c0/ff8d273055ef9b518529cfe80fd0486f7057a9917373807ff802d75ceb46e8f8e148f41fa094ee7625c8f34642cfaa98395ff182d9519898da7cbf383d4a210d + languageName: node + linkType: hard + "classnames@npm:2.x, classnames@npm:^2.2.1, classnames@npm:^2.2.3, classnames@npm:^2.2.5, classnames@npm:^2.2.6, classnames@npm:^2.3.1, classnames@npm:^2.3.2, classnames@npm:^2.5.1": version: 2.5.1 resolution: "classnames@npm:2.5.1" @@ -5010,6 +5112,88 @@ __metadata: languageName: node linkType: hard +"d3-color@npm:1 - 3": + version: 3.1.0 + resolution: "d3-color@npm:3.1.0" + checksum: 10c0/a4e20e1115fa696fce041fbe13fbc80dc4c19150fa72027a7c128ade980bc0eeeba4bcf28c9e21f0bce0e0dbfe7ca5869ef67746541dcfda053e4802ad19783c + languageName: node + linkType: hard + +"d3-dispatch@npm:1 - 3": + version: 3.0.1 + resolution: "d3-dispatch@npm:3.0.1" + checksum: 10c0/6eca77008ce2dc33380e45d4410c67d150941df7ab45b91d116dbe6d0a3092c0f6ac184dd4602c796dc9e790222bad3ff7142025f5fd22694efe088d1d941753 + languageName: node + linkType: hard + +"d3-drag@npm:2 - 3, d3-drag@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-drag@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-selection: "npm:3" + checksum: 10c0/d2556e8dc720741a443b595a30af403dd60642dfd938d44d6e9bfc4c71a962142f9a028c56b61f8b4790b65a34acad177d1263d66f103c3c527767b0926ef5aa + languageName: node + linkType: hard + +"d3-ease@npm:1 - 3": + version: 3.0.1 + resolution: "d3-ease@npm:3.0.1" + checksum: 10c0/fec8ef826c0cc35cda3092c6841e07672868b1839fcaf556e19266a3a37e6bc7977d8298c0fcb9885e7799bfdcef7db1baaba9cd4dcf4bc5e952cf78574a88b0 + languageName: node + linkType: hard + +"d3-interpolate@npm:1 - 3": + version: 3.0.1 + resolution: "d3-interpolate@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + checksum: 10c0/19f4b4daa8d733906671afff7767c19488f51a43d251f8b7f484d5d3cfc36c663f0a66c38fe91eee30f40327443d799be17169f55a293a3ba949e84e57a33e6a + languageName: node + linkType: hard + +"d3-selection@npm:2 - 3, d3-selection@npm:3, d3-selection@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-selection@npm:3.0.0" + checksum: 10c0/e59096bbe8f0cb0daa1001d9bdd6dbc93a688019abc97d1d8b37f85cd3c286a6875b22adea0931b0c88410d025563e1643019161a883c516acf50c190a11b56b + languageName: node + linkType: hard + +"d3-timer@npm:1 - 3": + version: 3.0.1 + resolution: "d3-timer@npm:3.0.1" + checksum: 10c0/d4c63cb4bb5461d7038aac561b097cd1c5673969b27cbdd0e87fa48d9300a538b9e6f39b4a7f0e3592ef4f963d858c8a9f0e92754db73116770856f2fc04561a + languageName: node + linkType: hard + +"d3-transition@npm:2 - 3": + version: 3.0.1 + resolution: "d3-transition@npm:3.0.1" + dependencies: + d3-color: "npm:1 - 3" + d3-dispatch: "npm:1 - 3" + d3-ease: "npm:1 - 3" + d3-interpolate: "npm:1 - 3" + d3-timer: "npm:1 - 3" + peerDependencies: + d3-selection: 2 - 3 + checksum: 10c0/4e74535dda7024aa43e141635b7522bb70cf9d3dfefed975eb643b36b864762eca67f88fafc2ca798174f83ca7c8a65e892624f824b3f65b8145c6a1a88dbbad + languageName: node + linkType: hard + +"d3-zoom@npm:^3.0.0": + version: 3.0.0 + resolution: "d3-zoom@npm:3.0.0" + dependencies: + d3-dispatch: "npm:1 - 3" + d3-drag: "npm:2 - 3" + d3-interpolate: "npm:1 - 3" + d3-selection: "npm:2 - 3" + d3-transition: "npm:2 - 3" + checksum: 10c0/ee2036479049e70d8c783d594c444fe00e398246048e3f11a59755cd0e21de62ece3126181b0d7a31bf37bcf32fd726f83ae7dea4495ff86ec7736ce5ad36fd3 + languageName: node + linkType: hard + "dashdash@npm:^1.12.0": version: 1.14.1 resolution: "dashdash@npm:1.14.1" @@ -15166,7 +15350,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.4.0": +"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.2, use-sync-external-store@npm:^1.4.0": version: 1.4.0 resolution: "use-sync-external-store@npm:1.4.0" peerDependencies: @@ -15861,6 +16045,26 @@ __metadata: languageName: node linkType: hard +"zustand@npm:^4.4.0": + version: 4.5.6 + resolution: "zustand@npm:4.5.6" + dependencies: + use-sync-external-store: "npm:^1.2.2" + peerDependencies: + "@types/react": ">=16.8" + immer: ">=9.0.6" + react: ">=16.8" + peerDependenciesMeta: + "@types/react": + optional: true + immer: + optional: true + react: + optional: true + checksum: 10c0/5b39aff2ef57e5a8ada647261ec1115697d397be311c51461d9ea81b5b63c6d2c498b960477ad2db72dc21db6aa229a92bdf644f6a8ecf7b1d71df5b4a5e95d3 + languageName: node + linkType: hard + "zwitch@npm:^1.0.0": version: 1.0.5 resolution: "zwitch@npm:1.0.5"