diff --git a/docs/changelog/update.md b/docs/changelog/update.md
index 34621064..d5aaa93e 100644
--- a/docs/changelog/update.md
+++ b/docs/changelog/update.md
@@ -3,7 +3,7 @@
💭 **Features Todo**
- [ ] LangGraph 升级到 0.6+ 版本,并适配新特性,如 context 等,添加 MCP 工具的支持。
- [x] 支持动态工具配置的同时,将 Configuration替换为 Context 后能够正常使用
-
+- [ ] 使用更好的知识图谱检索方法
- [ ] 使用其他的聊天记录管理方法,解决两个问题,一个是上下文长度过长,一个是上下文的类型变得更加丰富,比如多模态等等。(现在是基于 LangGraph 的 Memory 实现的,v0.2.3 版本实现),暂定使用 [mem0](github.com/mem0ai/mem0) 来实现。但是目前了解下来,还不是我想要的那种方案。可能会基于这个实现一个 ThreadConvManager 这个类。
- [ ] 添加对于上传文件的支持:这里的复杂的地方就在于如何和历史记录结合在一起(v0.2.3 版本实现,放在记忆管理后面)
diff --git a/web/src/components/AgentChatComponent.vue b/web/src/components/AgentChatComponent.vue
index 33a14c0e..afa1b1f8 100644
--- a/web/src/components/AgentChatComponent.vue
+++ b/web/src/components/AgentChatComponent.vue
@@ -180,7 +180,7 @@ const threadMessages = ref({});
const uiState = reactive({
...props.state,
debug_mode: computed(() => props.state.debug_mode ?? false),
- isSidebarOpen: localStorage.getItem('chat_sidebar_open') === 'true',
+ isSidebarOpen: localStorage.getItem('chat_sidebar_open', 'true') === 'true',
isInitialRender: true,
showRenameButton: false,
containerWidth: 0,
@@ -213,7 +213,7 @@ const currentThreadState = computed(() => {
const onGoingConvMessages = computed(() => {
const threadState = currentThreadState.value;
if (!threadState || !threadState.onGoingConv) return [];
-
+
const msgs = Object.values(threadState.onGoingConv.msgChunks).map(MessageProcessor.mergeMessageChunk);
return msgs.length > 0
? MessageProcessor.convertToolResultToMessages(msgs).filter(msg => msg.type !== 'tool')
@@ -335,9 +335,9 @@ const resetOnGoingConv = (threadId = null) => {
const _processStreamChunk = (chunk, threadId) => {
const { status, msg, request_id, message } = chunk;
const threadState = getThreadState(threadId);
-
+
if (!threadState) return;
-
+
switch (status) {
case 'init':
threadState.onGoingConv.msgChunks[request_id] = [msg];
diff --git a/web/src/views/GraphView.vue b/web/src/views/GraphView.vue
index 5cedd5f7..7da59ee4 100644
--- a/web/src/views/GraphView.vue
+++ b/web/src/views/GraphView.vue
@@ -11,7 +11,6 @@
@@ -21,9 +20,6 @@
Neo4j 浏览器
-
- 说明
-
上传文件
为{{ unindexedCount }}个节点添加索引
@@ -31,30 +27,46 @@
-
-
-
-
-
获取节点
+
-
-
{
const nodeSize = Math.min(15 + degree * 5, 50);
return {
- id: node.id,
+ id: String(node.id),
data: {
label: node.name,
degree: degree, // 存储度数信息
},
}
}),
- edges: graphData.edges.map(edge => {
+ edges: graphData.edges.map((edge, index) => {
return {
+ id: `edge-${index}`,
source: edge.source_id,
target: edge.target_id,
data: {
@@ -351,25 +364,149 @@ const initGraph = () => {
// 使用节点度数来决定大小
size: (d) => {
const degree = d.data.degree || 0;
- // 基础大小为15,每个连接增加5的大小,最小为15,最大为50
return Math.min(15 + degree * 5, 50);
},
+ // 现代化默认样式
+ fillOpacity: 0.8,
+ opacity: 0.8,
+ stroke: '#ffffff',
+ lineWidth: 1.5,
+ shadowColor: '#94a3b8',
+ shadowBlur: 4,
+ 'label-text-fill': '#334155',
},
palette: {
field: 'label',
- color: 'tableau',
+ color: [
+ '#60a5fa', // sky-400
+ '#34d399', // emerald-400
+ '#f59e0b', // amber-500
+ '#f472b6', // pink-400
+ '#22d3ee', // cyan-400
+ '#a78bfa', // violet-400
+ '#f97316', // orange-500
+ '#4ade80', // green-400
+ '#f43f5e', // rose-500
+ '#2dd4bf', // teal-400
+ ],
+ },
+ state: {
+ hidden: {
+ opacity: 0.15,
+ 'label-text-opacity': 0,
+ },
+ focus: {
+ opacity: 1,
+ stroke: '#2563eb', // blue-600
+ lineWidth: 2.5,
+ shadowColor: '#60a5fa',
+ shadowBlur: 16,
+ },
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
- labelBackground: '#fff',
+ labelBackground: '#ffffff',
+ stroke: '#94a3b8',
+ opacity: 0.6,
+ lineWidth: 1.2,
endArrow: true,
+ 'label-text-fill': '#334155',
+ },
+ state: {
+ hidden: {
+ opacity: 0.15,
+ 'label-text-opacity': 0,
+ },
+ focus: {
+ opacity: 0.95,
+ stroke: '#2563eb',
+ lineWidth: 2,
+ 'label-text-opacity': 1,
+ },
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
+
+ let activeNodeId = null;
+
+ const getGraphItemIds = () => {
+ const { nodes, edges } = graphInstance.getData();
+ const nodeIds = nodes.map(node => node.id);
+ const edgeIds = edges.map(edge => edge.id);
+ return { nodeIds, edgeIds };
+ };
+
+ // 从事件对象中尽可能稳健地获取被点击元素的 id
+ const getClickedElementId = (e) => {
+ return e?.id || e?.data?.id || e?.target?.id || null;
+ };
+
+ const resetStyles = async () => {
+ const { nodeIds, edgeIds } = getGraphItemIds();
+ const allElementIds = [...nodeIds, ...edgeIds];
+ const updates = {};
+ allElementIds.forEach(id => {
+ updates[id] = [];
+ });
+
+ if (allElementIds.length > 0) {
+ await graphInstance.setElementState(updates);
+ }
+ activeNodeId = null;
+ await graphInstance.draw();
+ };
+
+ graphInstance.on('node:click', async (e) => {
+ const clickedNodeId = getClickedElementId(e);
+ if (!clickedNodeId) return;
+
+ if (activeNodeId === clickedNodeId) {
+ await resetStyles();
+ return;
+ }
+
+ activeNodeId = clickedNodeId;
+
+ const { nodeIds: allNodeIds, edgeIds: allEdgeIds } = getGraphItemIds();
+ const { edges: allEdges } = graphInstance.getData();
+
+ const updates = {};
+
+ // 1. All nodes and edges are hidden by default
+ allNodeIds.forEach(id => updates[id] = ['hidden']);
+ allEdgeIds.forEach(id => updates[id] = ['hidden']);
+
+ // 2. Find neighbors and related edges
+ const neighborSet = new Set();
+ const relatedEdgeIds = [];
+ allEdges.forEach((edge) => {
+ if (edge.source === clickedNodeId) {
+ neighborSet.add(edge.target);
+ relatedEdgeIds.push(edge.id);
+ } else if (edge.target === clickedNodeId) {
+ neighborSet.add(edge.source);
+ relatedEdgeIds.push(edge.id);
+ }
+ });
+ const neighborNodeIds = Array.from(neighborSet);
+
+ // 3. Set 'focus' state for the clicked node, its neighbors, and related edges.
+ // This will overwrite the 'hidden' state.
+ updates[clickedNodeId] = ['focus'];
+ neighborNodeIds.forEach(id => updates[id] = ['focus']);
+ relatedEdgeIds.forEach(id => updates[id] = ['focus']);
+
+ // 4. Apply all state changes at once
+ await graphInstance.setElementState(updates);
+ await graphInstance.draw();
+ });
+
+ // 专门监听画布空白处的点击事件来重置样式
+ graphInstance.on('canvas:click', resetStyles);
window.addEventListener('resize', renderGraph);
}
@@ -399,13 +536,19 @@ const graphStatusText = computed(() => {
return graphInfo.value?.status === 'open' ? '已连接' : '已关闭';
});
-const graphDescription = computed(() => {
- const dbName = graphInfo.value?.graph_name || '';
- const entityCount = graphInfo.value?.entity_count || 0;
- const relationCount = graphInfo.value?.relationship_count || 0;
- const unindexed = unindexedCount.value > 0 ? `,${unindexedCount.value}个节点未索引` : '';
+// 新增:将图谱信息拆分为多条标签用于展示
+const graphTags = computed(() => {
+ const tags = [];
+ const dbName = graphInfo.value?.graph_name;
+ const entityCount = graphInfo.value?.entity_count;
+ const relationCount = graphInfo.value?.relationship_count;
- return `${dbName} - 共 ${entityCount} 实体,${relationCount} 个关系;${unindexed}`;
+ if (dbName) tags.push({ key: 'name', text: `图谱 ${dbName}`, type: 'blue' });
+ if (typeof entityCount === 'number') tags.push({ key: 'entities', text: `实体 ${graphData.nodes.length} of ${entityCount}`, type: 'success' });
+ if (typeof relationCount === 'number') tags.push({ key: 'relations', text: `关系 ${graphData.edges.length} of ${relationCount}`, type: 'purple' });
+ if (unindexedCount.value > 0) tags.push({ key: 'unindexed', text: `未索引 ${unindexedCount.value}`, type: 'warning' });
+
+ return tags;
});
// 为未索引节点添加索引
@@ -449,8 +592,14 @@ const openLink = (url) => {