ForcePilot/web/src/views/GraphView.vue
2024-09-01 18:10:50 +08:00

285 lines
6.4 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="graph-container" v-if="configStore.config.enable_knowledge_base">
<div class="info">
<h1>Neo4j 图数据库</h1>
<p>基于 Neo4j 构建的图数据库</p>
</div>
<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">
<div class="upload">
<a-upload-dragger
class="upload-dragger"
v-model:fileList="fileList"
name="file"
:fileList="fileList"
: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>
</div>
<div class="action-right">
<input
v-model="state.searchInput"
placeholder="输入要查询的实体"
style="width: 200px"
@keydown.enter="onSearch"
/>
<a-button
type="primary"
:loading="state.searchLoading"
@click="onSearch"
>
检索实体
</a-button>
</div>
</div>
<div class="main" id="container"></div>
</div>
<div class="database-empty" v-else>
<a-empty>
<template #description>
<span>
前往 <router-link to="/setting" style="color: var(--main-color); font-weight: bold;">设置</router-link> 页面配置知识库
</span>
</template>
</a-empty>
</div>
</template>
<script setup>
import { Graph } from "@antv/g6";
import { computed, onMounted, reactive, ref } from 'vue';
import { message } from "ant-design-vue";
import { useConfigStore } from '@/stores/config';
const configStore = useConfigStore()
let graphInstance
const fileList = ref([]);
const subgraph = reactive({
nodes: [
{ id: '1', name: 'node1' },
{ id: '2', name: 'node2' },
{ id: '3', name: 'node3' },
{ id: '4', name: 'node4' },
{ id: '5', name: 'node5' },
],
edges: [
{ id: 'e1', source_id: '1', target_id: '2', type: 'edge1' },
{ id: 'e2', source_id: '1', target_id: '3', type: 'edge2' },
{ id: 'e3', source_id: '2', target_id: '4', type: 'edge3' },
{ id: 'e4', source_id: '2', target_id: '5', type: 'edge4' },
],
});
const state = reactive({
searchInput: '',
searchLoading: false,
showModal: false,
precessing: false,
})
const getCurWidth = () => document.getElementById("container").offsetWidth
const getCurHeight = () => document.getElementById("container").offsetHeight
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)
};
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();
}
onMounted(() => {
graphInstance = new Graph({
container: document.getElementById("container"),
width: getCurWidth(),
height: getCurHeight(),
autoFit: true,
autoResize: true,
layout: {
type: 'force-atlas2',
preventOverlap: true,
kr: 100,
},
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'],
});
graphInstance.setData(graphData.value);
graphInstance.render();
window.addEventListener('resize', randerGraph);
});
const handleFileUpload = (event) => {
console.log(event)
console.log(fileList.value)
}
const handleDrop = (event) => {
console.log(event)
console.log(fileList.value)
}
</script>
<style lang="less" scoped>
.graph-container {
padding: 20px;
}
.actions {
display: flex;
justify-content: space-between;
margin-bottom: 20px;
input {
margin-right: 10px;
border-radius: 8px;
padding: 4px 12px;
border: 2px solid #d9d9d9;
outline: none;
height: 42px;
&:focus {
border-color: var(--main-color);
}
}
button {
height: 40px;
box-shadow: none;
}
}
.upload {
margin-bottom: 20px;
.upload-dragger {
margin: 0px;
}
}
#container {
background: #F7F7F7;
margin: 20px 0;
border-radius: 16px;
width: 100%;
height: calc(100% - 200px);
}
.database-empty {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
flex-direction: column;
color: var(--c-text-light-1);
}
</style>