parent
640ca19cba
commit
eba746a3bc
@ -64,6 +64,10 @@
|
|||||||
"@llm-tools/embedjs-loader-xml": "^0.1.28",
|
"@llm-tools/embedjs-loader-xml": "^0.1.28",
|
||||||
"@llm-tools/embedjs-openai": "^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",
|
"@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",
|
"adm-zip": "^0.5.16",
|
||||||
"docx": "^9.0.2",
|
"docx": "^9.0.2",
|
||||||
"electron-log": "^5.1.5",
|
"electron-log": "^5.1.5",
|
||||||
|
|||||||
@ -200,6 +200,25 @@
|
|||||||
"topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic",
|
"topics.prompt.tips": "Topic Prompts: Additional supplementary prompts provided for the current topic",
|
||||||
"topics.title": "Topics",
|
"topics.title": "Topics",
|
||||||
"topics.unpinned": "Unpinned 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"
|
"translate": "Translate"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
|
|||||||
@ -200,6 +200,25 @@
|
|||||||
"topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供",
|
"topics.prompt.tips": "トピック提示語:現在のトピックに対して追加の補足提示語を提供",
|
||||||
"topics.title": "トピック",
|
"topics.title": "トピック",
|
||||||
"topics.unpinned": "固定解除",
|
"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": "翻訳"
|
"translate": "翻訳"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
|
|||||||
@ -200,6 +200,25 @@
|
|||||||
"topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы",
|
"topics.prompt.tips": "Тематические подсказки: Дополнительные подсказки, предоставленные для текущей темы",
|
||||||
"topics.title": "Топики",
|
"topics.title": "Топики",
|
||||||
"topics.unpinned": "Открепленные темы",
|
"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": "Перевести"
|
"translate": "Перевести"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
|
|||||||
@ -200,6 +200,25 @@
|
|||||||
"topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词",
|
"topics.prompt.tips": "话题提示词: 针对当前话题提供额外的补充提示词",
|
||||||
"topics.title": "话题",
|
"topics.title": "话题",
|
||||||
"topics.unpinned": "取消固定",
|
"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": "翻译"
|
"translate": "翻译"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
|
|||||||
@ -200,6 +200,25 @@
|
|||||||
"topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞",
|
"topics.prompt.tips": "話題提示詞:針對目前話題提供額外的補充提示詞",
|
||||||
"topics.title": "話題",
|
"topics.title": "話題",
|
||||||
"topics.unpinned": "取消固定",
|
"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": "翻譯"
|
"translate": "翻譯"
|
||||||
},
|
},
|
||||||
"code_block": {
|
"code_block": {
|
||||||
|
|||||||
614
src/renderer/src/pages/home/Messages/ChatFlowHistory.tsx
Normal file
614
src/renderer/src/pages/home/Messages/ChatFlowHistory.tsx
Normal file
@ -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 = <Avatar src={data.userAvatar} alt={title} />
|
||||||
|
} else {
|
||||||
|
avatar = <Avatar icon={<UserOutlined />} 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 = <ModelAvatar model={data.modelInfo} size={32} />
|
||||||
|
} else if (data.modelId) {
|
||||||
|
const modelLogo = getModelLogo(data.modelId)
|
||||||
|
avatar = (
|
||||||
|
<Avatar
|
||||||
|
src={modelLogo}
|
||||||
|
icon={!modelLogo ? <RobotOutlined /> : undefined}
|
||||||
|
style={{ backgroundColor: 'var(--color-primary)' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
avatar = <Avatar icon={<RobotOutlined />} 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 (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
<TooltipContent>
|
||||||
|
<TooltipTitle>{title}</TooltipTitle>
|
||||||
|
<TooltipBody>{data.content}</TooltipBody>
|
||||||
|
<TooltipFooter>{t('chat.history.click_to_navigate')}</TooltipFooter>
|
||||||
|
</TooltipContent>
|
||||||
|
}
|
||||||
|
placement="top"
|
||||||
|
color="rgba(0, 0, 0, 0.85)"
|
||||||
|
mouseEnterDelay={0.3}
|
||||||
|
mouseLeaveDelay={0.1}
|
||||||
|
destroyTooltipOnHide>
|
||||||
|
<CustomNodeContainer
|
||||||
|
style={{
|
||||||
|
borderColor,
|
||||||
|
background: `linear-gradient(135deg, ${backgroundColor} 0%, ${gradientColor} 100%)`,
|
||||||
|
boxShadow: `0 4px 10px rgba(0, 0, 0, 0.1), 0 0 0 2px ${borderColor}40`
|
||||||
|
}}
|
||||||
|
onClick={handleNodeClick}>
|
||||||
|
<Handle type="target" position={Position.Top} style={handleStyle} isConnectable={false} />
|
||||||
|
<Handle type="target" position={Position.Left} style={handleStyle} isConnectable={false} />
|
||||||
|
|
||||||
|
<NodeHeader>
|
||||||
|
<NodeAvatar>{avatar}</NodeAvatar>
|
||||||
|
<NodeTitle>{title}</NodeTitle>
|
||||||
|
</NodeHeader>
|
||||||
|
<NodeContent title={data.content}>{data.content}</NodeContent>
|
||||||
|
|
||||||
|
<Handle type="source" position={Position.Bottom} style={handleStyle} isConnectable={false} />
|
||||||
|
<Handle type="source" position={Position.Right} style={handleStyle} isConnectable={false} />
|
||||||
|
</CustomNodeContainer>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建自定义节点类型
|
||||||
|
const nodeTypes: NodeTypes = { custom: CustomNode }
|
||||||
|
|
||||||
|
interface ChatFlowHistoryProps {
|
||||||
|
conversationId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义节点和边的类型
|
||||||
|
type FlowNode = Node<any, string>
|
||||||
|
type FlowEdge = Edge<any>
|
||||||
|
|
||||||
|
// 统一的边样式
|
||||||
|
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<ChatFlowHistoryProps> = ({ conversationId }) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [nodes, setNodes, onNodesChange] = useNodesState<any>([])
|
||||||
|
const [edges, setEdges, onEdgesChange] = useEdgesState<any>([])
|
||||||
|
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 (
|
||||||
|
<FlowContainer>
|
||||||
|
{loading ? (
|
||||||
|
<LoadingContainer>
|
||||||
|
<Spin size="large" />
|
||||||
|
</LoadingContainer>
|
||||||
|
) : nodes.length > 0 ? (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<div style={{ width: '100%', height: '100%' }}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
nodesDraggable={false}
|
||||||
|
nodesConnectable={false}
|
||||||
|
edgesFocusable={true}
|
||||||
|
zoomOnDoubleClick={true}
|
||||||
|
preventScrolling={true}
|
||||||
|
elementsSelectable={true}
|
||||||
|
selectNodesOnDrag={false}
|
||||||
|
nodesFocusable={true}
|
||||||
|
zoomOnScroll={true}
|
||||||
|
panOnScroll={false}
|
||||||
|
minZoom={0.4}
|
||||||
|
maxZoom={1}
|
||||||
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
|
fitView={true}
|
||||||
|
fitViewOptions={{
|
||||||
|
padding: 0.3,
|
||||||
|
includeHiddenNodes: false,
|
||||||
|
minZoom: 0.4,
|
||||||
|
maxZoom: 1
|
||||||
|
}}
|
||||||
|
proOptions={{ hideAttribution: true }}
|
||||||
|
className="react-flow-container">
|
||||||
|
<Controls showInteractive={false} />
|
||||||
|
<MiniMap
|
||||||
|
nodeStrokeWidth={3}
|
||||||
|
zoomable
|
||||||
|
pannable
|
||||||
|
nodeColor={(node) => (node.data.type === 'user' ? 'var(--color-info)' : 'var(--color-primary)')}
|
||||||
|
/>
|
||||||
|
</ReactFlow>
|
||||||
|
</div>
|
||||||
|
</ReactFlowProvider>
|
||||||
|
) : (
|
||||||
|
<EmptyContainer>
|
||||||
|
<EmptyText>{t('chat.history.no_messages')}</EmptyText>
|
||||||
|
</EmptyContainer>
|
||||||
|
)}
|
||||||
|
</FlowContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 样式组件定义
|
||||||
|
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
|
||||||
|
})
|
||||||
@ -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 { 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 { FC, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
|
|
||||||
|
import ChatFlowHistory from './ChatFlowHistory'
|
||||||
|
|
||||||
interface ChatNavigationProps {
|
interface ChatNavigationProps {
|
||||||
containerId: string
|
containerId: string
|
||||||
}
|
}
|
||||||
@ -14,6 +19,8 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
const [isVisible, setIsVisible] = useState(false)
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
const [isNearButtons, setIsNearButtons] = useState(false)
|
const [isNearButtons, setIsNearButtons] = useState(false)
|
||||||
const [hideTimer, setHideTimer] = useState<NodeJS.Timeout | null>(null)
|
const [hideTimer, setHideTimer] = useState<NodeJS.Timeout | null>(null)
|
||||||
|
const [showChatHistory, setShowChatHistory] = useState(false)
|
||||||
|
const currentTopicId = useSelector((state: RootState) => selectCurrentTopicId(state))
|
||||||
const lastMoveTime = useRef(0)
|
const lastMoveTime = useRef(0)
|
||||||
const { topicPosition, showTopics } = useSettings()
|
const { topicPosition, showTopics } = useSettings()
|
||||||
const showRightTopics = topicPosition === 'right' && showTopics
|
const showRightTopics = topicPosition === 'right' && showTopics
|
||||||
@ -59,6 +66,15 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
setHideTimer(timer)
|
setHideTimer(timer)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleChatHistoryClick = () => {
|
||||||
|
setShowChatHistory(true)
|
||||||
|
resetHideTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDrawerClose = () => {
|
||||||
|
setShowChatHistory(false)
|
||||||
|
}
|
||||||
|
|
||||||
const findUserMessages = () => {
|
const findUserMessages = () => {
|
||||||
const container = document.getElementById(containerId)
|
const container = document.getElementById(containerId)
|
||||||
if (!container) return []
|
if (!container) return []
|
||||||
@ -259,31 +275,58 @@ const ChatNavigation: FC<ChatNavigationProps> = ({ containerId }) => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NavigationContainer
|
<>
|
||||||
$isVisible={isVisible}
|
<NavigationContainer
|
||||||
$right={right}
|
$isVisible={isVisible}
|
||||||
onMouseEnter={handleMouseEnter}
|
$right={right}
|
||||||
onMouseLeave={handleMouseLeave}>
|
onMouseEnter={handleMouseEnter}
|
||||||
<ButtonGroup>
|
onMouseLeave={handleMouseLeave}>
|
||||||
<Tooltip title={t('chat.navigation.prev')} placement="left">
|
<ButtonGroup>
|
||||||
<NavigationButton
|
<Tooltip title={t('chat.navigation.prev')} placement="left">
|
||||||
type="text"
|
<NavigationButton
|
||||||
icon={<UpOutlined />}
|
type="text"
|
||||||
onClick={handlePrevMessage}
|
icon={<UpOutlined />}
|
||||||
aria-label={t('chat.navigation.prev')}
|
onClick={handlePrevMessage}
|
||||||
/>
|
aria-label={t('chat.navigation.prev')}
|
||||||
</Tooltip>
|
/>
|
||||||
<Divider />
|
</Tooltip>
|
||||||
<Tooltip title={t('chat.navigation.next')} placement="left">
|
<Divider />
|
||||||
<NavigationButton
|
<Tooltip title={t('chat.navigation.next')} placement="left">
|
||||||
type="text"
|
<NavigationButton
|
||||||
icon={<DownOutlined />}
|
type="text"
|
||||||
onClick={handleNextMessage}
|
icon={<DownOutlined />}
|
||||||
aria-label={t('chat.navigation.next')}
|
onClick={handleNextMessage}
|
||||||
/>
|
aria-label={t('chat.navigation.next')}
|
||||||
</Tooltip>
|
/>
|
||||||
</ButtonGroup>
|
</Tooltip>
|
||||||
</NavigationContainer>
|
<Divider />
|
||||||
|
<Tooltip title={t('chat.navigation.history')} placement="left">
|
||||||
|
<NavigationButton
|
||||||
|
type="text"
|
||||||
|
icon={<HistoryOutlined />}
|
||||||
|
onClick={handleChatHistoryClick}
|
||||||
|
aria-label={t('chat.navigation.history')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ButtonGroup>
|
||||||
|
</NavigationContainer>
|
||||||
|
|
||||||
|
<Drawer
|
||||||
|
title={t('chat.history.title')}
|
||||||
|
placement="right"
|
||||||
|
onClose={handleDrawerClose}
|
||||||
|
open={showChatHistory}
|
||||||
|
width={680}
|
||||||
|
destroyOnClose
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 0,
|
||||||
|
height: 'calc(100% - 55px)'
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<ChatFlowHistory conversationId={currentTopicId || undefined} />
|
||||||
|
</Drawer>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import Scrollbar from '@renderer/components/Scrollbar'
|
import Scrollbar from '@renderer/components/Scrollbar'
|
||||||
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
import { useMessageOperations } from '@renderer/hooks/useMessageOperations'
|
||||||
import { useSettings } from '@renderer/hooks/useSettings'
|
import { useSettings } from '@renderer/hooks/useSettings'
|
||||||
|
import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
|
||||||
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'
|
||||||
import { classNames } from '@renderer/utils'
|
import { classNames } from '@renderer/utils'
|
||||||
@ -36,6 +37,38 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
|||||||
setSelectedIndex(messageLength - 1)
|
setSelectedIndex(messageLength - 1)
|
||||||
}, [messageLength])
|
}, [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(
|
const setSelectedMessage = useCallback(
|
||||||
(message: Message) => {
|
(message: Message) => {
|
||||||
messages.forEach(async (m) => {
|
messages.forEach(async (m) => {
|
||||||
@ -47,11 +80,47 @@ const MessageGroup = ({ messages, topic, hidePresetMessages }: Props) => {
|
|||||||
if (messageElement) {
|
if (messageElement) {
|
||||||
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
messageElement.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||||
}
|
}
|
||||||
}, 100)
|
}, 200)
|
||||||
},
|
},
|
||||||
[editMessage, messages]
|
[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(
|
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
|
||||||
|
|||||||
206
yarn.lock
206
yarn.lock
@ -2798,6 +2798,71 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.6":
|
||||||
version: 4.1.12
|
version: 4.1.12
|
||||||
resolution: "@types/debug@npm:4.1.12"
|
resolution: "@types/debug@npm:4.1.12"
|
||||||
@ -3306,6 +3371,35 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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:.":
|
"CherryStudio@workspace:.":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "CherryStudio@workspace:."
|
resolution: "CherryStudio@workspace:."
|
||||||
@ -3355,6 +3449,7 @@ __metadata:
|
|||||||
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
"@types/react-infinite-scroll-component": "npm:^5.0.0"
|
||||||
"@types/tinycolor2": "npm:^1"
|
"@types/tinycolor2": "npm:^1"
|
||||||
"@vitejs/plugin-react": "npm:^4.2.1"
|
"@vitejs/plugin-react": "npm:^4.2.1"
|
||||||
|
"@xyflow/react": "npm:^12.4.4"
|
||||||
adm-zip: "npm:^0.5.16"
|
adm-zip: "npm:^0.5.16"
|
||||||
antd: "npm:^5.22.5"
|
antd: "npm:^5.22.5"
|
||||||
applescript: "npm:^1.0.0"
|
applescript: "npm:^1.0.0"
|
||||||
@ -4592,6 +4687,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"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
|
version: 2.5.1
|
||||||
resolution: "classnames@npm:2.5.1"
|
resolution: "classnames@npm:2.5.1"
|
||||||
@ -5010,6 +5112,88 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"dashdash@npm:^1.12.0":
|
||||||
version: 1.14.1
|
version: 1.14.1
|
||||||
resolution: "dashdash@npm:1.14.1"
|
resolution: "dashdash@npm:1.14.1"
|
||||||
@ -15166,7 +15350,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 1.4.0
|
||||||
resolution: "use-sync-external-store@npm:1.4.0"
|
resolution: "use-sync-external-store@npm:1.4.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -15861,6 +16045,26 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"zwitch@npm:^1.0.0":
|
||||||
version: 1.0.5
|
version: 1.0.5
|
||||||
resolution: "zwitch@npm:1.0.5"
|
resolution: "zwitch@npm:1.0.5"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user