ForcePilot/web/src/views/GraphView.vue

360 lines
8.2 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>
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link>
</span>
</template>
</a-empty>
</div>
<div class="graph-container layout-container" v-else>
2024-07-24 18:32:14 +08:00
<div class="info">
2024-09-06 12:54:17 +08:00
<h2>图数据库 {{ graph?.database_name }}</h2>
<p>
<span v-if="state.graphloading">加载中</span>
<span class="green-dot" v-if="graph?.status == 'open'"></span>
<span class="red-dot" v-else></span>
<span>{{ graph?.status }}</span> ·
<span> {{ graph?.entity_count }} 实体{{ graph?.relationship_count }} 个关系</span>
</p>
</div>
2024-07-24 18:32:14 +08:00
<div class="actions">
<div class="actions-left">
<a-button @click="state.showModal = true">上传文件</a-button>
<a-modal
:open="state.showModal" title="上传文件"
@ok="addDocumentByFile"
@cancel="() => state.showModal = false"
ok-text="添加到图数据库" cancel-text="取消"
:confirm-loading="state.precessing">
2024-07-24 18:32:14 +08:00
<div class="upload">
<a-upload-dragger
class="upload-dragger"
v-model:fileList="fileList"
name="file"
:fileList="fileList"
2024-07-24 18:32:14 +08:00
:max-count="1"
:disabled="state.precessing"
action="/api/database/upload"
@change="handleFileUpload"
@drop="handleDrop"
>
<p class="ant-upload-text">点击或者把文件拖拽到这里上传</p>
<p class="ant-upload-hint">
目前仅支持上传文本文件 .pdf, .txt, .md且同名文件无法重复添加
</p>
</a-upload-dragger>
</div>
</a-modal>
2024-09-06 12:54:17 +08:00
<input v-model="sampleNodeCount">
<a-button @click="loadSampleNodes">确定</a-button>
2024-07-24 18:32:14 +08:00
</div>
<div class="action-right">
<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"
@click="onSearch"
>
检索实体
</a-button>
</div>
</div>
2024-09-06 12:54:17 +08:00
<div class="main" id="container" ref="container"></div>
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';
import { message } from "ant-design-vue";
2024-09-01 18:10:50 +08:00
import { useConfigStore } from '@/stores/config';
const configStore = useConfigStore()
2024-07-24 18:32:14 +08:00
let graphInstance
2024-09-06 12:54:17 +08:00
const graph = ref(null)
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-07-24 18:32:14 +08:00
const subgraph = reactive({
2024-09-06 12:54:17 +08:00
nodes: [],
edges: [],
2024-07-24 18:32:14 +08:00
});
const state = reactive({
2024-09-06 12:54:17 +08:00
graphloading: false,
2024-07-24 18:32:14 +08:00
searchInput: '',
searchLoading: false,
showModal: false,
precessing: 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
2024-09-03 16:37:59 +08:00
2024-09-06 12:54:17 +08:00
const loadGraph = () => {
state.graphloading = true
fetch('/api/database/graph', {
method: "GET",
})
.then(response => response.json())
.then(data => {
console.log(data)
graph.value = data.graph
state.graphloading = false
})
.catch(error => {
console.error(error)
message.error(error.message)
state.graphloading = false
})
}
2024-07-17 18:52:20 +08:00
2024-07-24 18:32:14 +08:00
const graphData = computed(() => {
return {
nodes: subgraph.nodes.map(node => {
return {
id: node.id,
data: {
label: node.name
},
}
}),
edges: subgraph.edges.map(edge => {
return {
source: edge.source_id,
target: edge.target_id,
data: {
label: edge.type
}
}
}),
}
})
const addDocumentByFile = () => {
state.precessing = true
const files = fileList.value.filter(file => file.status === 'done').map(file => file.response.file_path)
fetch('/api/database/graph/add', {
method: 'POST',
body: JSON.stringify({
file_path: files[0]
}),
})
.then(response => response.json())
.then((data) => {
message.success(data.message);
state.showModal = false;
})
.catch((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 = () => {
fetch(`/api/database/graph/nodes?kgdb_name=neo4j&num=${sampleNodeCount.value}`)
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw new Error("加载失败");
}
})
.then((data) => {
subgraph.nodes = data.result.nodes
subgraph.edges = data.result.edges
console.log(data)
console.log(subgraph)
setTimeout(() => {
randerGraph()
}, 500)
})
.catch((error) => {
message.error(error.message);
})
}
2024-07-24 18:32:14 +08:00
const onSearch = () => {
if (!state.searchInput) {
message.error('请输入要查询的实体')
return
}
state.searchLoading = true
fetch(`/api/database/graph/node?entity_name=${state.searchInput}`)
.then((res) => {
if (res.ok) {
return res.json();
} else {
throw new Error("查询失败");
}
})
.then((data) => {
subgraph.nodes = data.result.nodes
subgraph.edges = data.result.edges
console.log(data)
console.log(subgraph)
randerGraph()
})
.catch((error) => {
message.error(error.message);
})
.finally(() => state.searchLoading = false)
};
const randerGraph = () => {
graphInstance.setData(graphData.value);
graphInstance.render();
}
2024-07-17 18:52:20 +08:00
onMounted(() => {
2024-09-06 12:54:17 +08:00
loadGraph();
loadSampleNodes();
setTimeout(() => {
if (state.showPage) {
graphInstance = new Graph({
container: container.value,
width: container.value.offsetWidth,
height: container.value.offsetHeight,
autoFit: true,
autoResize: true,
layout: {
type: 'd3-force',
preventOverlap: true,
kr: 100,
collide: {
strength: 0.5,
},
},
node: {
type: 'circle',
style: {
labelText: (d) => d.data.label,
size: 40,
},
palette: {
field: 'label',
color: 'tableau',
},
},
edge: {
type: 'line',
style: {
labelText: (d) => d.data.label,
labelBackground: '#fff',
},
},
behaviors: ['drag-element', 'zoom-canvas', 'drag-canvas'],
});
graphInstance.setData(graphData.value);
graphInstance.render();
window.addEventListener('resize', randerGraph);
}
}, 400)
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-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 {
.info span.green-dot, .info span.red-dot {
display: inline-block;
width: 14px;
height: 14px;
border-radius: 50%;
margin: 0 5px;
}
.info span.green-dot {
background: #52c41a;
}
.info span.red-dot {
background: #f5222d;
}
.info {
margin-bottom: 20px;
}
}
2024-07-24 18:32:14 +08:00
.actions {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
2024-09-06 12:54:17 +08:00
.actions-left {
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
margin-right: 10px;
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 {
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;
margin: 20px 0;
border-radius: 16px;
2024-07-17 18:52:20 +08:00
width: 100%;
2024-09-06 12:54:17 +08:00
height: 800px;
resize: horizontal;
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;
color: var(--c-text-light-1);
}
2024-07-17 18:52:20 +08:00
</style>