ForcePilot/web/src/views/GraphView.vue

505 lines
13 KiB
Vue
Raw Normal View History

2024-07-17 18:52:20 +08:00
<template>
2024-09-06 12:54:17 +08:00
<div class="database-empty" v-if="!state.showPage">
<a-empty>
<template #description>
<span>
2025-02-26 10:57:24 +08:00
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link>
2024-09-06 12:54:17 +08:00
</span>
</template>
</a-empty>
</div>
<div class="graph-container layout-container" v-else>
2024-09-14 02:46:44 +08:00
<HeaderComponent
title="图数据库"
2025-02-28 14:14:00 +08:00
:description="graphDescription"
2024-09-14 02:46:44 +08:00
>
<template #actions>
<div class="status-wrapper">
<div class="status-indicator" :class="graphStatusClass"></div>
</div>
<a-button type="primary" @click="state.showModal = true" ><UploadOutlined/> 上传文件</a-button>
2025-03-08 22:49:22 +08:00
<a-button v-if="unindexedCount > 0" type="primary" @click="indexNodes" :loading="state.indexing">
<SyncOutlined/> {{ unindexedCount }}个节点添加索引
</a-button>
2024-09-14 02:46:44 +08:00
</template>
</HeaderComponent>
2024-07-24 18:32:14 +08:00
<div class="actions">
<div class="actions-left">
<input
v-model="state.searchInput"
placeholder="输入要查询的实体"
style="width: 200px"
@keydown.enter="onSearch"
2024-07-24 18:32:14 +08:00
/>
<a-button
type="primary"
:loading="state.searchLoading"
2025-02-28 14:14:00 +08:00
:disabled="state.searchLoading"
2024-07-24 18:32:14 +08:00
@click="onSearch"
>
检索实体
</a-button>
</div>
2024-09-14 02:46:44 +08:00
<div class="actions-right">
<input v-model="sampleNodeCount">
<a-button @click="loadSampleNodes" :loading="state.fetching">获取节点</a-button>
</div>
2024-07-24 18:32:14 +08:00
</div>
2024-09-09 17:07:03 +08:00
<div class="main" id="container" ref="container" v-show="graphData.nodes.length > 0"></div>
2025-02-23 16:39:52 +08:00
<a-empty v-show="graphData.nodes.length === 0" style="padding: 4rem 0;"/>
2024-09-14 02:46:44 +08:00
<a-modal
:open="state.showModal" title="上传文件"
@ok="addDocumentByFile"
@cancel="() => state.showModal = false"
ok-text="添加到图数据库" cancel-text="取消"
2025-02-28 02:39:47 +08:00
:ok-button-props="{ disabled: disabled }"
2024-09-14 02:46:44 +08:00
:confirm-loading="state.precessing">
2025-02-23 16:39:52 +08:00
<div v-if="graphInfo?.embed_model_name">
<a-alert v-if="!modelMatched" message="模型不匹配,构建索引可能会出现无法检索到的情况!" type="warning" />
<p>
当前图数据库向量模型{{ graphInfo?.embed_model_name }}
当前所选择的向量模型是 {{ cur_embed_model }}
</p>
2025-02-23 16:39:52 +08:00
</div>
2025-02-28 02:39:47 +08:00
<p v-else>第一次创建之后将无法修改向量模型当前向量模型 {{ cur_embed_model }}</p>
2024-09-14 02:46:44 +08:00
<div class="upload">
<a-upload-dragger
class="upload-dragger"
v-model:fileList="fileList"
name="file"
:fileList="fileList"
:max-count="1"
2025-02-28 02:39:47 +08:00
:disabled="disabled"
2024-10-02 20:11:28 +08:00
action="/api/data/upload"
2025-05-09 23:46:15 +08:00
:headers="getAuthHeaders()"
2024-09-14 02:46:44 +08:00
@change="handleFileUpload"
@drop="handleDrop"
>
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
<p class="ant-upload-hint">
目前仅支持上传 jsonl 文件且同名文件无法重复添加
</p>
</a-upload-dragger>
</div>
</a-modal>
2024-09-01 18:10:50 +08:00
</div>
2024-07-17 18:52:20 +08:00
</template>
<script setup>
import { Graph } from "@antv/g6";
2024-07-24 18:32:14 +08:00
import { computed, onMounted, reactive, ref } from 'vue';
2024-09-14 02:46:44 +08:00
import { message, Button as AButton } from 'ant-design-vue';
2024-09-01 18:10:50 +08:00
import { useConfigStore } from '@/stores/config';
2025-03-08 22:49:22 +08:00
import { UploadOutlined, SyncOutlined } from '@ant-design/icons-vue';
2024-09-14 02:46:44 +08:00
import HeaderComponent from '@/components/HeaderComponent.vue';
2025-05-02 23:56:59 +08:00
import { graphApi } from '@/apis/admin_api';
2025-05-09 23:46:15 +08:00
import { useUserStore } from '@/stores/user';
2024-09-01 18:10:50 +08:00
2025-05-02 23:56:59 +08:00
const configStore = useConfigStore();
const cur_embed_model = computed(() => configStore.config?.embed_model_names?.[configStore.config?.embed_model]?.name || '');
const modelMatched = computed(() => !graphInfo?.value?.embed_model_name || graphInfo.value.embed_model_name === cur_embed_model.value)
const disabled = computed(() => state.precessing || !modelMatched.value)
2024-07-24 18:32:14 +08:00
let graphInstance
2024-09-06 21:30:49 +08:00
const graphInfo = ref(null)
2024-09-06 12:54:17 +08:00
const container = ref(null);
2024-07-24 18:32:14 +08:00
const fileList = ref([]);
2024-09-06 12:54:17 +08:00
const sampleNodeCount = ref(100);
2024-09-06 21:30:49 +08:00
const graphData = reactive({
2024-09-06 12:54:17 +08:00
nodes: [],
edges: [],
2024-07-24 18:32:14 +08:00
});
const state = reactive({
2024-09-09 17:07:03 +08:00
fetching: false,
2024-09-06 21:30:49 +08:00
loadingGraphInfo: false,
2024-07-24 18:32:14 +08:00
searchInput: '',
searchLoading: false,
showModal: false,
precessing: false,
2025-03-08 22:49:22 +08:00
indexing: false,
2024-09-03 16:37:59 +08:00
showPage: computed(() => configStore.config.enable_knowledge_base && configStore.config.enable_knowledge_graph),
2024-07-24 18:32:14 +08:00
})
2024-07-17 18:52:20 +08:00
2025-03-08 22:49:22 +08:00
// 计算未索引节点数量
const unindexedCount = computed(() => {
return graphInfo.value?.unindexed_node_count || 0;
});
2024-09-03 16:37:59 +08:00
2024-09-06 21:30:49 +08:00
const loadGraphInfo = () => {
state.loadingGraphInfo = true
2025-05-02 23:56:59 +08:00
graphApi.getGraphInfo()
2024-09-06 12:54:17 +08:00
.then(data => {
console.log(data)
graphInfo.value = data
2024-09-06 21:30:49 +08:00
state.loadingGraphInfo = false
2024-09-06 12:54:17 +08:00
})
.catch(error => {
console.error(error)
2025-05-02 23:56:59 +08:00
message.error(error.message || '加载图数据库信息失败')
2024-09-06 21:30:49 +08:00
state.loadingGraphInfo = false
2024-09-06 12:54:17 +08:00
})
}
2024-07-17 18:52:20 +08:00
2024-09-06 21:30:49 +08:00
const getGraphData = () => {
2025-03-05 16:31:27 +08:00
// 计算每个节点的度数(连接数)
const nodeDegrees = {};
2025-03-08 22:49:22 +08:00
2025-03-05 16:31:27 +08:00
// 初始化所有节点的度数为0
graphData.nodes.forEach(node => {
nodeDegrees[node.id] = 0;
});
2025-03-08 22:49:22 +08:00
2025-03-05 16:31:27 +08:00
// 计算每个节点的连接数
graphData.edges.forEach(edge => {
nodeDegrees[edge.source_id] = (nodeDegrees[edge.source_id] || 0) + 1;
nodeDegrees[edge.target_id] = (nodeDegrees[edge.target_id] || 0) + 1;
});
2025-03-08 22:49:22 +08:00
2024-07-24 18:32:14 +08:00
return {
2024-09-06 21:30:49 +08:00
nodes: graphData.nodes.map(node => {
2025-03-05 16:31:27 +08:00
// 计算节点大小基础大小为15每个连接增加5的大小最小为15最大为50
const degree = nodeDegrees[node.id] || 0;
const nodeSize = Math.min(15 + degree * 5, 50);
2025-03-08 22:49:22 +08:00
2024-07-24 18:32:14 +08:00
return {
id: node.id,
data: {
2025-03-05 16:31:27 +08:00
label: node.name,
degree: degree, // 存储度数信息
2024-07-24 18:32:14 +08:00
},
}
}),
2024-09-06 21:30:49 +08:00
edges: graphData.edges.map(edge => {
2024-07-24 18:32:14 +08:00
return {
source: edge.source_id,
target: edge.target_id,
data: {
label: edge.type
}
}
}),
}
2024-09-06 21:30:49 +08:00
}
2024-07-24 18:32:14 +08:00
const addDocumentByFile = () => {
state.precessing = true
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path)
2025-05-02 23:56:59 +08:00
graphApi.addByJsonl(files[0])
.then((data) => {
if (data.status === 'success') {
message.success(data.message);
state.showModal = false;
} else {
throw new Error(data.message);
}
})
.catch((error) => {
console.error(error)
message.error(error.message || '添加文件失败');
})
.finally(() => state.precessing = false)
2024-07-24 18:32:14 +08:00
};
2024-09-06 12:54:17 +08:00
const loadSampleNodes = () => {
2024-09-09 17:07:03 +08:00
state.fetching = true
2025-05-02 23:56:59 +08:00
graphApi.getNodes('neo4j', sampleNodeCount.value)
2024-09-06 12:54:17 +08:00
.then((data) => {
2024-09-06 21:30:49 +08:00
graphData.nodes = data.result.nodes
graphData.edges = data.result.edges
console.log(graphData)
2025-02-23 16:39:52 +08:00
setTimeout(() => randerGraph(), 500)
2024-09-06 12:54:17 +08:00
})
.catch((error) => {
2025-05-02 23:56:59 +08:00
console.error(error)
message.error(error.message || '加载节点失败');
if (configStore?.config && !configStore?.config.enable_knowledge_graph) {
message.error('请前往设置页面配置启用知识图谱')
}
2024-09-06 12:54:17 +08:00
})
2024-09-09 17:07:03 +08:00
.finally(() => state.fetching = false)
2024-09-06 12:54:17 +08:00
}
2024-07-24 18:32:14 +08:00
const onSearch = () => {
2025-02-28 14:14:00 +08:00
if (state.searchLoading) {
message.error('请稍后再试')
return
}
if (graphInfo?.value?.embed_model_name !== cur_embed_model.value) {
2025-05-02 23:56:59 +08:00
// if (!graphInfo?.value?.embed_model_name) {
// message.error('请先上传文件(jsonl)')
// return
// }
2025-02-28 14:14:00 +08:00
if (!confirm(`构建图数据库时向量模型为 ${graphInfo?.value?.embed_model_name},当前向量模型为 ${cur_embed_model.value},是否继续查询?`)) {
return
}
}
2024-07-24 18:32:14 +08:00
if (!state.searchInput) {
message.error('请输入要查询的实体')
return
}
state.searchLoading = true
2025-05-02 23:56:59 +08:00
graphApi.queryNode(state.searchInput)
2024-07-24 18:32:14 +08:00
.then((data) => {
2024-09-14 02:46:44 +08:00
if (!data.result || !data.result.nodes || !data.result.edges) {
throw new Error('返回数据格式不正确');
}
2024-09-06 21:30:49 +08:00
graphData.nodes = data.result.nodes
graphData.edges = data.result.edges
2024-09-09 17:07:03 +08:00
if (graphData.nodes.length === 0) {
message.info('未找到相关实体')
}
2024-07-24 18:32:14 +08:00
console.log(data)
2024-09-06 21:30:49 +08:00
console.log(graphData)
2024-07-24 18:32:14 +08:00
randerGraph()
})
.catch((error) => {
2024-09-14 02:46:44 +08:00
console.error('查询错误:', error);
2025-05-02 23:56:59 +08:00
message.error(`查询出错:${error.message || '未知错误'}`);
2024-07-24 18:32:14 +08:00
})
.finally(() => state.searchLoading = false)
};
const randerGraph = () => {
2024-09-06 21:30:49 +08:00
if (graphInstance) {
graphInstance.destroy();
}
initGraph();
graphInstance.setData(getGraphData());
2024-07-24 18:32:14 +08:00
graphInstance.render();
}
2024-09-06 21:30:49 +08:00
const initGraph = () => {
graphInstance = new Graph({
container: container.value,
width: container.value.offsetWidth,
height: container.value.offsetHeight,
autoFit: true,
autoResize: true,
layout: {
type: 'd3-force',
preventOverlap: true,
collide: {
2025-03-05 16:31:27 +08:00
radius: 40,
strength: 0.5, // 碰撞强度
2024-09-06 21:30:49 +08:00
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
2025-03-05 16:31:27 +08:00
// 使用节点度数来决定大小
size: (d) => {
const degree = d.data.degree || 0;
// 基础大小为15每个连接增加5的大小最小为15最大为50
return Math.min(15 + degree * 5, 50);
},
2024-09-06 21:30:49 +08:00
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
endArrow: true,
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
window.addEventListener('resize', randerGraph);
}
2024-07-17 18:52:20 +08:00
onMounted(() => {
2024-09-06 21:30:49 +08:00
loadGraphInfo();
2024-09-06 12:54:17 +08:00
loadSampleNodes();
2024-07-17 18:52:20 +08:00
});
2024-07-24 18:32:14 +08:00
const handleFileUpload = (event) => {
console.log(event)
console.log(fileList.value)
}
const handleDrop = (event) => {
console.log(event)
console.log(fileList.value)
}
2024-09-14 02:46:44 +08:00
const graphStatusClass = computed(() => {
if (state.loadingGraphInfo) return 'loading';
return graphInfo.value?.status === 'open' ? 'open' : 'closed';
});
const graphStatusText = computed(() => {
if (state.loadingGraphInfo) return '加载中';
return graphInfo.value?.status === 'open' ? '已连接' : '已关闭';
});
2025-02-28 14:14:00 +08:00
const graphDescription = computed(() => {
const dbName = graphInfo.value?.graph_name || '';
2025-02-28 14:14:00 +08:00
const entityCount = graphInfo.value?.entity_count || 0;
const relationCount = graphInfo.value?.relationship_count || 0;
const modelName = graphInfo.value?.embed_model_name || '未上传文件';
2025-03-08 22:49:22 +08:00
const unindexed = unindexedCount.value > 0 ? `${unindexedCount.value}个节点未索引` : '';
2025-02-28 14:14:00 +08:00
2025-03-08 22:49:22 +08:00
return `${dbName} - 共 ${entityCount} 实体,${relationCount} 个关系。向量模型:${modelName}${unindexed}`;
2025-02-28 14:14:00 +08:00
});
2025-03-08 22:49:22 +08:00
// 为未索引节点添加索引
const indexNodes = () => {
// 判断 embed_model_name 是否相同
if (!modelMatched.value) {
message.error(`向量模型不匹配,无法添加索引,当前向量模型为 ${cur_embed_model.value},图数据库向量模型为 ${graphInfo.value?.embed_model_name}`)
return
}
if (state.precessing) {
message.error('后台正在处理,请稍后再试')
return
}
state.indexing = true;
2025-05-02 23:56:59 +08:00
graphApi.indexNodes('neo4j')
.then(data => {
message.success(data.message || '索引添加成功');
// 刷新图谱信息
loadGraphInfo();
})
.catch(error => {
console.error(error);
message.error(error.message || '添加索引失败');
})
.finally(() => {
state.indexing = false;
});
2025-03-08 22:49:22 +08:00
};
2025-05-09 23:46:15 +08:00
const getAuthHeaders = () => {
const userStore = useUserStore();
return userStore.getAuthHeaders();
};
2024-07-17 18:52:20 +08:00
</script>
2024-07-24 18:32:14 +08:00
<style lang="less" scoped>
2024-09-06 12:54:17 +08:00
.graph-container {
2024-09-14 02:46:44 +08:00
padding: 0;
}
2024-09-06 12:54:17 +08:00
2024-09-14 02:46:44 +08:00
.status-wrapper {
display: flex;
align-items: center;
margin-right: 16px;
font-size: 14px;
color: rgba(0, 0, 0, 0.65);
}
2024-09-06 12:54:17 +08:00
2024-09-14 02:46:44 +08:00
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
&.loading {
background-color: #faad14;
animation: pulse 1.5s infinite ease-in-out;
2024-09-06 12:54:17 +08:00
}
2024-09-14 02:46:44 +08:00
&.open {
background-color: #52c41a;
2024-09-06 12:54:17 +08:00
}
2024-09-14 02:46:44 +08:00
&.closed {
background-color: #f5222d;
2024-09-06 12:54:17 +08:00
}
}
2024-09-14 02:46:44 +08:00
@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0.5;
}
50% {
transform: scale(1.2);
opacity: 1;
}
100% {
transform: scale(0.8);
opacity: 0.5;
}
}
2024-09-06 12:54:17 +08:00
2024-07-24 18:32:14 +08:00
.actions {
display: flex;
justify-content: space-between;
2024-09-14 02:46:44 +08:00
margin: 20px 0;
padding: 0 24px;
2024-07-24 18:32:14 +08:00
2024-09-06 21:30:49 +08:00
.actions-left, .actions-right {
2024-09-06 12:54:17 +08:00
display: flex;
align-items: center;
gap: 10px;
}
2024-07-24 18:32:14 +08:00
input {
2024-09-06 12:54:17 +08:00
width: 100px;
2024-07-24 18:32:14 +08:00
border-radius: 8px;
padding: 4px 12px;
2024-09-06 12:54:17 +08:00
border: 2px solid var(--main-300);
2024-07-24 18:32:14 +08:00
outline: none;
height: 42px;
&:focus {
border-color: var(--main-color);
}
}
button {
2024-09-06 21:30:49 +08:00
border-width: 2px;
2024-07-24 18:32:14 +08:00
height: 40px;
box-shadow: none;
}
}
.upload {
margin-bottom: 20px;
.upload-dragger {
margin: 0px;
}
}
2024-07-17 18:52:20 +08:00
#container {
2024-07-24 18:32:14 +08:00
background: #F7F7F7;
2024-09-14 02:46:44 +08:00
margin: 20px 24px;
2024-07-24 18:32:14 +08:00
border-radius: 16px;
2024-09-14 02:46:44 +08:00
width: calc(100% - 48px);
2024-09-09 17:07:03 +08:00
height: calc(100vh - 200px);
2024-09-06 12:54:17 +08:00
resize: horizontal;
2024-09-09 17:07:03 +08:00
overflow: hidden;
2024-07-17 18:52:20 +08:00
}
2024-09-01 18:10:50 +08:00
.database-empty {
display: flex;
justify-content: center;
align-items: center;
2024-09-03 16:37:59 +08:00
width: 100%;
2024-09-01 18:10:50 +08:00
height: 100%;
flex-direction: column;
2024-09-25 13:46:51 +08:00
color: var(--gray-900);
2024-09-01 18:10:50 +08:00
}
2024-07-17 18:52:20 +08:00
</style>