This commit is contained in:
Wenjie Zhang 2024-07-17 18:52:20 +08:00
parent 6c5c099132
commit 86e8910eee
18 changed files with 1651 additions and 111 deletions

View File

@ -34,9 +34,23 @@ class Config(SimpleConfig):
self.filename = filename self.filename = filename
logger.info(f"Loading config from {filename}") logger.info(f"Loading config from {filename}")
### startup ### >>> 默认配置
# 可以在 config/base.yaml 中覆盖
self.mode = "cli" self.mode = "cli"
self.stream = False self.stream = True
# 功能选项
self.enable_query_rewrite = True
self.enable_knowledge_base = True
self.enable_knowledge_graph = True
self.enable_search_engine = True
# 模型配置
## 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
## 如果需要自定义路径,则在 config/base.yaml 中配置 model_local_paths
self.embed_model = "bge-large-zh-v1.5"
self.reranker = "bge-reranker-v2-m3"
### <<< 默认配置结束
self.load() self.load()
self.handle_self() self.handle_self()
@ -51,6 +65,7 @@ class Config(SimpleConfig):
def load(self): def load(self):
"""根据传入的文件覆盖掉默认配置"""
if self.filename is not None and os.path.exists(self.filename): if self.filename is not None and os.path.exists(self.filename):
if self.filename.endswith(".json"): if self.filename.endswith(".json"):
with open(self.filename, 'r') as f: with open(self.filename, 'r') as f:

View File

@ -1,25 +1,12 @@
# 默认配置请参考 config/__init__.py
name: base name: base
## model ## model
### model_provider, option in deepseek, zhipu ### model_provider, option in deepseek, zhipu
model_provider: qianfan model_provider: qianfan
# 注意这里是模型名,而不是具体的模型路径,默认使用 HuggingFace 的路径
# 如果需要自定义路径,则在下面配置 model_local_paths
embed_model: bge-large-zh # option in ["bge-large-zh"]
## startup
stream: True
## knowledge
enable_query_rewrite: True
enable_knowledge_base: True
enable_knowledge_graph: True
enable_search_engine: True
## model dir 可以写相对路径和绝对路径 ## model dir 可以写相对路径和绝对路径
### 相对路径是相对于环境变量中 MODEL_ROOT_DIR 的路径 ### 相对路径是相对于环境变量中 MODEL_ROOT_DIR 的路径
model_local_paths: model_local_paths:
bge-large-zh: bge-large-zh-v1.5 bge-large-zh-v1.5: bge-large-zh-v1.5
oneke: oneke oneke: oneke

View File

@ -1,7 +1,8 @@
import os import os
import json import json
import time import time
from utils import hashstr, setup_logger from utils import hashstr, setup_logger, is_text_pdf
from plugins import pdf2txt
from core.knowledgebase import KnowledgeBase from core.knowledgebase import KnowledgeBase
from core.filereader import pdfreader, plainreader from core.filereader import pdfreader, plainreader
from core.graphbase import GraphDatabase from core.graphbase import GraphDatabase
@ -146,17 +147,24 @@ class DataBaseManager:
support_format = [".pdf", ".txt", "*.md"] support_format = [".pdf", ".txt", "*.md"]
assert os.path.exists(file), "File not found" assert os.path.exists(file), "File not found"
logger.info(f"Try to read file {file}") logger.info(f"Try to read file {file}")
if os.path.isfile(file):
if not os.path.isfile(file):
logger.error(f"Directory not supported now!")
raise NotImplementedError("Directory not supported now!")
if file.endswith(".pdf"): if file.endswith(".pdf"):
if is_text_pdf(file):
return pdfreader(file) return pdfreader(file)
else:
return pdf2txt(file, return_text=True)
elif file.endswith(".txt") or file.endswith(".md"): elif file.endswith(".txt") or file.endswith(".md"):
return plainreader(file) return plainreader(file)
else: else:
logger.error(f"File format not supported, only support {support_format}") logger.error(f"File format not supported, only support {support_format}")
raise Exception(f"File format not supported, only support {support_format}") raise Exception(f"File format not supported, only support {support_format}")
else:
logger.error(f"Directory not supported now!")
raise NotImplementedError("Directory not supported now!")
def delete_file(self, db_id, file_id): def delete_file(self, db_id, file_id):
db = self.get_kb_by_id(db_id) db = self.get_kb_by_id(db_id)
@ -187,6 +195,16 @@ class DataBaseManager:
chunks.append(text[i:i + chunk_size]) chunks.append(text[i:i + chunk_size])
return chunks return chunks
def delete_database(self, db_id):
db = self.get_kb_by_id(db_id)
if db is None:
return {"message": "database not found"}, 404
self.knowledge_base.client.drop_collection(db.metaname)
self.data["databases"] = [d for d in self.data["databases"] if d.db_id != db_id]
self._save_databases()
return {"message": "删除成功"}
def get_kb_by_id(self, db_id): def get_kb_by_id(self, db_id):
for db in self.data["databases"]: for db in self.data["databases"]:
if db.db_id == db_id: if db.db_id == db_id:

View File

@ -66,8 +66,7 @@ class KnowledgeBase:
res = self.client.insert(collection_name=collection_name, data=data) res = self.client.insert(collection_name=collection_name, data=data)
return res return res
def search(self, query, collection_name, limit=None): def search(self, query, collection_name, limit=3):
limit = limit or self.default_query_limit
query_vectors = self.embed_model.encode_queries([query]) query_vectors = self.embed_model.encode_queries([query])

View File

@ -1,32 +1,39 @@
from core.startup import dbm, model from core.startup import dbm, model
from models.embedding import ReRanker
class Retriever: class Retriever:
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.reranker = ReRanker(config)
def retrieval(self, query, history): def retrieval(self, query, history, meta):
refs = {} refs = {}
# TODO: 查询分类、查询重写、查询分解、查询伪文档生成HyDE) # TODO: 查询分类、查询重写、查询分解、查询伪文档生成HyDE)
# NOTE2024-07-14 暂时禁用知识检索 refs["knowledge_base"] = self.query_knowledgebase(query, history, meta)
refs["graph_base"] = self.query_graph(query, history, meta)
return refs return refs
def construct_query(self, query, refs): def construct_query(self, query, refs, meta):
# TODOReranking
if len(refs) == 0: if len(refs) == 0:
return query return query
external = "" external = ""
kb_res = refs.get("knowledge_base") kb_res = refs.get("knowledge_base").get("results", [])
if kb_res: if len(kb_res) > 0:
kb_text = "\n".join([f"{r['id']}: {r['entity']['text']}" for r in kb_res]) kb_text = "\n".join([f"{r['id']}: {r['entity']['text']}" for r in kb_res])
external += f"知识库信息: \n\n{kb_text}" external += f"知识库信息: \n\n{kb_text}"
db_res = refs.get("graph_base").get("results", [])
if len(db_res) > 0:
db_text = "\n".join([f"{r['id']}: {r['entity']['text']}" for r in db_res])
external += f"图数据库信息: \n\n{db_text}"
if len(external) > 0: if len(external) > 0:
query = f"以下是参考资料:\n\n\n{external}\n\n\n请根据前面的知识回答:{query}" query = f"以下是参考资料:\n\n\n{external}\n\n\n请根据前面的知识回答:{query}"
@ -39,16 +46,29 @@ class Retriever:
""" """
raise NotImplementedError raise NotImplementedError
def query_graph(self, query, history): def query_graph(self, query, history, meta):
# res = model.predict("qiansdgsa, dasdh ashdsakjdk ak ").content # res = model.predict("qiansdgsa, dasdh ashdsakjdk ak ").content
return {} return {}
def query_knowledgebase(self, query, history, meta):
kb_res = None
if meta.get("db_name"):
kb_res = dbm.knowledge_base.search(query, meta["db_name"], limit=5)
for r in kb_res:
r["rerank_score"] = self.reranker.compute_score([query, r["entity"]["text"]], normalize=True)
kb_res.sort(key=lambda x: x["rerank_score"], reverse=True)
final_res = [_res for _res in kb_res if _res["rerank_score"] > 0.1]
return {"results": final_res, "all_results": kb_res}
def rewrite_query(self, query): def rewrite_query(self, query):
"""重写查询""" """重写查询"""
raise NotImplementedError raise NotImplementedError
def __call__(self, query, history): def __call__(self, query, history, meta):
refs = self.retrieval(query, history) refs = self.retrieval(query, history, meta)
query = self.construct_query(query, refs) query = self.construct_query(query, refs, meta)
return query, refs return query, refs

View File

@ -1,4 +1,4 @@
from FlagEmbedding import FlagModel from FlagEmbedding import FlagModel, FlagReranker
from utils.logging_config import setup_logger from utils.logging_config import setup_logger
@ -6,11 +6,15 @@ from utils.logging_config import setup_logger
logger = setup_logger("EmbeddingModel") logger = setup_logger("EmbeddingModel")
SUPPORT_LIST = { SUPPORT_LIST = {
"bge-large-zh": "BAAI/bge-large-zh-v1.5", "bge-large-zh-v1.5": "BAAI/bge-large-zh-v1.5",
}
RERANKER_LIST = {
"bge-reranker-v2-m3": "BAAI/bge-reranker-v2-m3",
} }
QUERY_INSTRUCTION = { QUERY_INSTRUCTION = {
"bge-large-zh": "为这个句子生成表示以用于检索相关文章:", "bge-large-zh-v1.5": "为这个句子生成表示以用于检索相关文章:",
} }
class EmbeddingModel(FlagModel): class EmbeddingModel(FlagModel):
@ -26,3 +30,15 @@ class EmbeddingModel(FlagModel):
use_fp16=False, **kwargs) use_fp16=False, **kwargs)
logger.info(f"Embedding model {config.embed_model} loaded") logger.info(f"Embedding model {config.embed_model} loaded")
class ReRanker(FlagReranker):
def __init__(self, config, **kwargs):
assert config.reranker in RERANKER_LIST.keys(), f"Unsupported ReRanker: {config.reranker}, only support {RERANKER_LIST.keys()}"
model_name_or_path = config.model_local_paths.get(config.reranker, RERANKER_LIST[config.reranker])
logger.info(f"Loading ReRanker model {config.re_ranker} from {model_name_or_path}")
super().__init__(model_name_or_path, use_fp16=True, **kwargs)
logger.info(f"ReRanker model {config.re_ranker} loaded")

View File

@ -7,7 +7,7 @@ from copy import deepcopy
from tqdm import tqdm from tqdm import tqdm
def pdf2txt(pdf_path): def pdf2txt(pdf_path, return_text=False):
output_dir = os.path.join('tmp', 'pdf2txt', os.path.basename(pdf_path).split('.')[0]) output_dir = os.path.join('tmp', 'pdf2txt', os.path.basename(pdf_path).split('.')[0])
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
@ -63,6 +63,9 @@ def pdf2txt(pdf_path):
with open(respath, 'w', encoding='utf-8') as f: with open(respath, 'w', encoding='utf-8') as f:
f.write(whole_text) f.write(whole_text)
if return_text:
return whole_text
return respath return respath
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -31,10 +31,11 @@ def chat_get():
def chat(): def chat():
request_data = json.loads(request.data) request_data = json.loads(request.data)
query = request_data['query'] query = request_data['query']
meta = request_data.get('meta')
logger.debug(f"Web query: {query}") logger.debug(f"Web query: {query}")
history_manager = HistoryManager(request_data['history']) history_manager = HistoryManager(request_data['history'])
new_query, refs = retriever(query, history_manager.messages) new_query, refs = retriever(query, history_manager.messages, meta)
messages = history_manager.get_history_with_msg(new_query) messages = history_manager.get_history_with_msg(new_query)
history_manager.add_user(query) history_manager.add_user(query)
@ -63,3 +64,7 @@ def call():
return jsonify({ return jsonify({
"response": response.content, "response": response.content,
}) })
@common.route('/config', methods=['get'])
def get_config():
return jsonify(config)

View File

@ -28,6 +28,15 @@ def create_database():
database = dbm.create_database(database_name, description, db_type) database = dbm.create_database(database_name, description, db_type)
return jsonify(database) return jsonify(database)
# TODO: 删除数据库
@db.route('/', methods=['DELETE'])
def delete_database():
data = json.loads(request.data)
db_id = data.get('db_id')
logger.debug(f"Delete database {db_id}")
dbm.delete_database(db_id)
return jsonify({"message": "删除成功"})
@db.route('/add_by_file', methods=['POST']) @db.route('/add_by_file', methods=['POST'])
def create_document_by_file(): def create_document_by_file():
@ -53,9 +62,6 @@ def get_database_info():
return jsonify(database) return jsonify(database)
@db.route('/info', methods=['DELETE'])
def delete_database():
return jsonify({"message": "unimplemented"}), 501
@db.route('/document', methods=['DELETE']) @db.route('/document', methods=['DELETE'])
def delete_document(): def delete_document():

1300
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,8 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons-vue": "^6.1.0", "@ant-design/icons-vue": "^6.1.0",
"@antv/g6": "^5.0.9",
"@vueuse/core": "^10.11.0",
"ant-design-vue": "^4.2.3", "ant-design-vue": "^4.2.3",
"axios": "^1.3.4", "axios": "^1.3.4",
"d3": "^7.8.3", "d3": "^7.8.3",

View File

@ -19,7 +19,24 @@
</div> </div>
</div> </div>
<div class="header__right"> <div class="header__right">
<div class="nav-btn text" @click="myAlert('未开发')">张文杰</div> <a-dropdown>
<a class="ant-dropdown-link nav-btn text" @click.prevent>
<component :is="state.selectedKB === null ? BookOutlined : BookFilled" />&nbsp;
{{ state.selectedKB === null ? '未选择' : state.databases[state.selectedKB]?.name }}
</a>
<template #overlay>
<a-menu>
<a-menu-item v-for="(db, index) in state.databases" :key="index">
<a href="javascript:;" @click="state.selectedKB=index">{{ db.name }}</a>
</a-menu-item>
<a-menu-item >
<a href="javascript:;" @click="state.selectedKB = null">不使用</a>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<div class="nav-btn text" @click="state.showPanel = !state.showPanel">张文杰</div>
<div v-if="state.showPanel" class="my-panal" ref="panel">暂时不知道干嘛的地方</div>
</div> </div>
</div> </div>
<div v-if="conv.messages.length == 0" class="chat-examples"> <div v-if="conv.messages.length == 0" class="chat-examples">
@ -43,7 +60,10 @@
:class="message.role" :class="message.role"
> >
<p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p> <p v-if="message.role=='sent'" style="white-space: pre-line" class="message-text">{{ message.text }}</p>
<p v-else v-html="renderMarkdown(message.text)" class="message-md" ></p> <p v-else
v-html="renderMarkdown(message.text)"
class="message-md"
@click="consoleMsg(message)"></p>
</div> </div>
</div> </div>
<div class="input-box"> <div class="input-box">
@ -63,7 +83,15 @@
<script setup> <script setup>
import { reactive, ref, onMounted, toRefs } from 'vue' import { reactive, ref, onMounted, toRefs } from 'vue'
import { SendOutlined, MenuOutlined, FormOutlined, LoadingOutlined } from '@ant-design/icons-vue' import { onClickOutside } from '@vueuse/core'
import {
SendOutlined,
MenuOutlined,
FormOutlined,
LoadingOutlined,
BookOutlined,
BookFilled,
} from '@ant-design/icons-vue'
import { marked } from 'marked'; import { marked } from 'marked';
const props = defineProps({ const props = defineProps({
@ -76,9 +104,10 @@ const emit = defineEmits(['renameTitle'])
const { conv, state } = toRefs(props) const { conv, state } = toRefs(props)
const chatBox = ref(null) const chatBox = ref(null)
const isStreaming = ref(false) const isStreaming = ref(false)
const panel = ref(null)
const examples = ref([ const examples = ref([
'写一个冒泡排序', '写一个冒泡排序',
'介绍一下 MECT', '肉碱是什么?',
'介绍一下江南大学', '介绍一下江南大学',
'A大于BB小于CA和C哪个大', 'A大于BB小于CA和C哪个大',
'今天天气怎么样?' '今天天气怎么样?'
@ -91,6 +120,8 @@ marked.setOptions({
// marked https://marked.js.org/ // marked https://marked.js.org/
}); });
onClickOutside(panel, () => setTimeout(() => state.value.showPanel = false, 30))
const renameTitle = () => { const renameTitle = () => {
const prompt = '请用一个很短的句子关于下面的对话内容的主题起一个名字,不要带标点符号:' const prompt = '请用一个很短的句子关于下面的对话内容的主题起一个名字,不要带标点符号:'
const firstUserMessage = conv.value.messages[0].text const firstUserMessage = conv.value.messages[0].text
@ -101,13 +132,8 @@ const renameTitle = () => {
}) })
} }
const myAlert = (message) => { const myAlert = (message) => alert(message)
alert(message) const renderMarkdown = (text) => marked(text)
}
const renderMarkdown = (text) => {
return marked(text)
}
const scrollToBottom = () => { const scrollToBottom = () => {
setTimeout(() => { setTimeout(() => {
@ -115,6 +141,8 @@ const scrollToBottom = () => {
}, 10) }, 10)
} }
const consoleMsg = (message) => console.log(message)
const generateRandomHash = (length) => { const generateRandomHash = (length) => {
let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; let chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let hash = ''; let hash = '';
@ -192,6 +220,9 @@ const sendMessage = () => {
body: JSON.stringify({ body: JSON.stringify({
query: user_input, query: user_input,
history: conv.value.history, history: conv.value.history,
meta: {
db_name: state.value.databases[state.value.selectedKB]?.metaname
}
}), }),
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
@ -274,11 +305,20 @@ onMounted(() => {
padding: 1rem; padding: 1rem;
} }
.chat div.header .header__left { .chat div.header {
user-select: none;
.header__left, .header__right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
} }
}
.ant-dropdown-link {
color: var(--c-text-light-1);
cursor: pointer;
}
.nav-btn { .nav-btn {
font-size: 1.2rem; font-size: 1.2rem;
@ -302,6 +342,19 @@ onMounted(() => {
} }
} }
.my-panal {
position: absolute;
top: 100%;
right: 0;
margin-top: 5px;
background-color: white;
border: 1px solid #ccc;
box-shadow: 0px 0px 10px 1px rgba(0, 0, 0, 0.05);
border-radius: 12px;
padding: 12px;
z-index: 100;
}
div.chat-examples { div.chat-examples {
padding: 0 50px; padding: 0 50px;

View File

@ -45,6 +45,12 @@ const router = createRouter({
path: ':database_id', path: ':database_id',
name: 'databaseInfo', name: 'databaseInfo',
component: () => import('../views/DataBaseInfoView.vue'), component: () => import('../views/DataBaseInfoView.vue'),
},
{
path: 'graph',
name: 'graph',
component: () => import('../views/GraphView.vue'),
meta: { keepAlive: true }
} }
] ]
}, },

View File

@ -39,6 +39,9 @@ const convs = reactive(JSON.parse(localStorage.getItem('chat-convs')) || [
const state = reactive({ const state = reactive({
isSidebarOpen: true, isSidebarOpen: true,
selectedKB: null,
showPanel: false,
databases: [],
}) })
const curConvId = ref(0) const curConvId = ref(0)
@ -86,6 +89,18 @@ const delConv = (index) => {
} }
} }
const loadDatabases = () => {
fetch('/api/database/', {
method: "GET",
})
.then(response => response.json())
.then(data => {
console.log(data)
state.databases = data.databases
}
)
}
// Watch convs and save to localStorage // Watch convs and save to localStorage
watch( watch(
() => convs, () => convs,
@ -97,6 +112,7 @@ watch(
// Load convs from localStorage on mount // Load convs from localStorage on mount
onMounted(() => { onMounted(() => {
loadDatabases()
const savedSonvs = JSON.parse(localStorage.getItem('chat-convs')) const savedSonvs = JSON.parse(localStorage.getItem('chat-convs'))
if (savedSonvs) { if (savedSonvs) {
for (let i = 0; i < savedSonvs.length; i++) { for (let i = 0; i < savedSonvs.length; i++) {
@ -167,6 +183,7 @@ onMounted(() => {
padding: 16px; padding: 16px;
cursor: pointer; cursor: pointer;
width: 100%; width: 100%;
user-select: none;
&__title { &__title {
white-space: nowrap; /* 禁止换行 */ white-space: nowrap; /* 禁止换行 */

View File

@ -1,7 +1,11 @@
<template> <template>
<div style="display: flex;"> <div style="display: flex;">
<div class="sider"> <div class="sider">
<a-button type="link" @click="backToDatabase"><LeftOutlined />返回知识库</a-button> <div class="sider-top">
<div class="header-actions">
<a-button type="text" @click="backToDatabase"><LeftOutlined /></a-button>
<a-button type="text" danger class="del-db" @click="deleteDatabse"><DeleteOutlined /></a-button>
</div>
<div class="top"> <div class="top">
<div class="icon"><ReadFilled /></div> <div class="icon"><ReadFilled /></div>
<div class="info"> <div class="info">
@ -11,6 +15,9 @@
</div> </div>
<p class="description">{{ database.description }}</p> <p class="description">{{ database.description }}</p>
</div> </div>
<div class="sider-bottom">
</div>
</div>
<div class="db-info-container"> <div class="db-info-container">
<h2>向知识库中添加文件</h2> <h2>向知识库中添加文件</h2>
<div class="upload"> <div class="upload">
@ -59,7 +66,11 @@
<ClockCircleFilled style="color: #FFCD43;"/> <ClockCircleFilled style="color: #FFCD43;"/>
</template> </template>
<template v-else-if="column.key === 'action'"> <template v-else-if="column.key === 'action'">
<a-button class="del-btn" type="link" @click="deleteFile(text)" :disabled="state.lock">删除</a-button> <a-button class="del-btn" type="link"
@click="deleteFile(text)"
:disabled="state.lock || record.status != 'done' "
>删除
</a-button>
</template> </template>
<span v-else-if="column.key === 'created_at'">{{ formatRelativeTime(Math.round(text*1000)) }}</span> <span v-else-if="column.key === 'created_at'">{{ formatRelativeTime(Math.round(text*1000)) }}</span>
<span v-else>{{ text }}</span> <span v-else>{{ text }}</span>
@ -84,7 +95,7 @@
<script setup> <script setup>
import { onMounted, reactive, ref, watch } from 'vue'; import { onMounted, reactive, ref, watch } from 'vue';
import { message } from 'ant-design-vue'; import { message, Modal } from 'ant-design-vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { import {
ReadFilled, ReadFilled,
@ -93,6 +104,7 @@ import {
HourglassFilled, HourglassFilled,
CloseCircleFilled, CloseCircleFilled,
ClockCircleFilled, ClockCircleFilled,
DeleteOutlined,
} from '@ant-design/icons-vue' } from '@ant-design/icons-vue'
@ -109,6 +121,7 @@ const state = reactive({
refrashing: false, refrashing: false,
lock: false, lock: false,
drawer: false, drawer: false,
refreshInterval: null,
}); });
const handleFileUpload = (event) => { const handleFileUpload = (event) => {
@ -139,6 +152,41 @@ const handleRefresh = () => {
}) })
} }
const deleteDatabse = () => {
Modal.confirm({
title: '删除数据库',
content: '确定要删除该数据库吗?',
okText: '确认',
cancelText: '取消',
onOk: () => {
state.lock = true
fetch('/api/database/', {
method: "DELETE",
body: JSON.stringify({
db_id: databaseId.value
}),
})
.then(response => response.json())
.then(data => {
console.log(data)
message.success(data.message)
router.push('/database')
})
.catch(error => {
console.error(error)
message.error(error.message)
})
.finally(() => {
state.lock = false
})
},
onCancel: () => {
console.log('Cancel');
},
});
}
const openFileDetail = (record) => { const openFileDetail = (record) => {
state.lock = true state.lock = true
fetch(`/api/database/document?db_id=${databaseId.value}&file_id=${record.file_id}`, { fetch(`/api/database/document?db_id=${databaseId.value}&file_id=${record.file_id}`, {
@ -230,7 +278,7 @@ const addDocumentByFile = () => {
state.loading = true state.loading = true
state.lock = true state.lock = true
const refreshInterval = setInterval(() => { state.refreshInterval = setInterval(() => {
getDatabaseInfo(); getDatabaseInfo();
}, 1000); }, 1000);
fetch('/api/database/add_by_file', { fetch('/api/database/add_by_file', {
@ -252,7 +300,7 @@ const addDocumentByFile = () => {
}) })
.finally(() => { .finally(() => {
getDatabaseInfo() getDatabaseInfo()
clearInterval(refreshInterval) clearInterval(state.refreshInterval)
state.loading = false state.loading = false
}) })
} }
@ -293,7 +341,8 @@ const columns = [
watch(() => route.params.database_id, (newId) => { watch(() => route.params.database_id, (newId) => {
databaseId.value = newId; databaseId.value = newId;
console.log(newId) console.log(newId)
getDatabaseInfo(); clearInterval(state.refreshInterval)
getDatabaseInfo()
} }
); );
@ -308,13 +357,34 @@ onMounted(() => {
<style lang="less" scoped> <style lang="less" scoped>
.sider { .sider {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 300px; width: 300px;
height: 100%; height: 100%;
padding: 20px; padding: 0;
border-right: 1px solid #E0EAFF; border-right: 1px solid #E0EAFF;
button { .sider-top {
& > * {
padding: 0 20px;
}
.header-actions {
display: flex;
justify-content: space-between;
margin-bottom: 20px; margin-bottom: 20px;
padding-top: 10px;
padding-bottom: 10px;
background-color: #FAFAFA;
border-bottom: 1px solid #E0EAFF;
button {
height: auto;
font-size: 16px;
color: var(--c-text-light-1);
}
}
} }
} }
@ -394,9 +464,16 @@ onMounted(() => {
} }
} }
button.del-btn:hover { button.del-btn {
cursor: pointer; cursor: pointer;
&:hover {
color: var(--error-color); color: var(--error-color);
} }
&:disabled {
cursor: not-allowed;
}
}
} }
</style> </style>

View File

@ -45,7 +45,7 @@
<h2>图数据库</h2> <h2>图数据库</h2>
<p>基于 neo4j 构建的图数据库</p> <p>基于 neo4j 构建的图数据库</p>
<div :class="{'graphloading': graphloading}"> <div :class="{'graphloading': graphloading}">
<div class="dbcard graphbase" > <div class="dbcard graphbase" @click="navigateToGraph">
<div class="top"> <div class="top">
<div class="icon"><AppstoreFilled /></div> <div class="icon"><AppstoreFilled /></div>
<div class="info"> <div class="info">
@ -64,11 +64,12 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, reactive } from 'vue' import { ref, onMounted, reactive, watch } from 'vue'
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { message, Button } from 'ant-design-vue' import { message, Button } from 'ant-design-vue'
import { ReadFilled, PlusOutlined, AppstoreFilled } from '@ant-design/icons-vue' import { ReadFilled, PlusOutlined, AppstoreFilled } from '@ant-design/icons-vue'
const route = useRoute()
const router = useRouter() const router = useRouter()
const databases = ref([]) const databases = ref([])
const graph = ref(null) const graph = ref(null)
@ -80,7 +81,7 @@ const newDatabase = reactive({
loading: false, loading: false,
}) })
const load_databases = () => { const loadDatabases = () => {
loadGraph() loadGraph()
fetch('/api/database/', { fetch('/api/database/', {
method: "GET", method: "GET",
@ -112,7 +113,7 @@ const createDatabase = () => {
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
console.log(data) console.log(data)
load_databases() loadDatabases()
newDatabase.open = false newDatabase.open = false
newDatabase.name = '' newDatabase.name = ''
newDatabase.description = '' newDatabase.description = ''
@ -126,6 +127,10 @@ const navigateToDatabase = (databaseId) => {
router.push({ path: `/database/${databaseId}` }); router.push({ path: `/database/${databaseId}` });
}; };
const navigateToGraph = () => {
router.push({ path: `/database/graph` });
};
const loadGraph = () => { const loadGraph = () => {
graphloading.value = true graphloading.value = true
fetch('/api/database/graph', { fetch('/api/database/graph', {
@ -144,8 +149,14 @@ const loadGraph = () => {
}) })
} }
watch(() => route.path, (newPath, oldPath) => {
if (newPath === '/database') {
loadDatabases();
}
});
onMounted(() => { onMounted(() => {
load_databases() loadDatabases()
}) })
</script> </script>

View File

@ -0,0 +1,48 @@
<template>
<div class="graph-container">
<div class="main" id="container"></div>
</div>
</template>
<script setup>
import { Graph } from "@antv/g6";
import { onMounted } from 'vue';
const getCurWidth = () => document.getElementById("container").offsetWidth
const getCurHeight = () => document.getElementById("container").offsetHeight
onMounted(() => {
const graph = new Graph({
container: document.getElementById("container"),
width: getCurWidth(),
height: getCurHeight(),
data: {
nodes: [
{
id: "node-1",
style: { x: 50, y: 100 },
},
{
id: "node-2",
style: { x: 150, y: 100 },
},
],
edges: [{ id: "edge-1", source: "node-1", target: "node-2" }],
},
behaviors: ["drag-canvas", "zoom-canvas", "drag-element"],
});
graph.render();
});
</script>
<style scoped>
.graph-container {}
#container {
width: 100%;
height: 100%;
}
</style>

View File

@ -20,5 +20,8 @@ export default defineConfig({
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
} }
}, },
watch: {
ignored: ['**/node_modules/**', '**/dist/**'],
},
} }
}) })