feat: 增加 LightRAG的图谱可视化功能(但是目前的检索和可视化会出现跨知识库的情况,需要继续debug)
This commit is contained in:
parent
c89ba37617
commit
8f37e1c8d9
@ -6,7 +6,7 @@ services:
|
||||
args:
|
||||
http_proxy: ${HTTP_PROXY:-}
|
||||
https_proxy: ${HTTPS_PROXY:-}
|
||||
image: yuxi-api:0.1.0
|
||||
image: yuxi-api:0.2.0.dev
|
||||
container_name: api-dev
|
||||
working_dir: /app
|
||||
volumes:
|
||||
@ -68,7 +68,7 @@ services:
|
||||
context: .
|
||||
dockerfile: docker/web.Dockerfile
|
||||
target: development
|
||||
image: yuxi-web:0.1.0
|
||||
image: yuxi-web:0.2.0.dev
|
||||
container_name: web-dev
|
||||
volumes:
|
||||
- ./web:/app
|
||||
|
||||
@ -1,21 +1,25 @@
|
||||
# 开发阶段
|
||||
FROM node:latest AS development
|
||||
FROM node:20-alpine AS development
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
ARG http_proxy
|
||||
ARG https_proxy
|
||||
ENV http_proxy=$http_proxy \
|
||||
https_proxy=$https_proxy \
|
||||
TZ=Asia/Shanghai
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
# 复制 package.json 和 package-lock.json(如果存在)
|
||||
|
||||
# 只有当代理变量不为空时才设置代理
|
||||
RUN if [ -n "$http_proxy" ]; then echo "export http_proxy=$http_proxy" >> /etc/environment; fi
|
||||
RUN if [ -n "$https_proxy" ]; then echo "export https_proxy=$https_proxy" >> /etc/environment; fi
|
||||
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm@latest
|
||||
|
||||
# 复制 package.json 和 pnpm-lock.yaml
|
||||
COPY ./web/package*.json ./
|
||||
COPY ./web/pnpm-lock.yaml* ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm install -g pnpm@latest-10
|
||||
RUN pnpm install
|
||||
# RUN npm install --registry http://mirrors.cloud.tencent.com/npm/ --verbose --force
|
||||
|
||||
# 复制源代码
|
||||
COPY ./web .
|
||||
@ -26,15 +30,22 @@ EXPOSE 5173
|
||||
# 启动开发服务器的命令在 docker-compose 文件中定义
|
||||
|
||||
# 生产阶段
|
||||
FROM node:latest AS build-stage
|
||||
FROM node:20-alpine AS build-stage
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./web/package*.json ./
|
||||
RUN npm install --force
|
||||
# RUN npm install --registry https://registry.npmmirror.com --force
|
||||
# 安装 pnpm
|
||||
RUN npm install -g pnpm@latest
|
||||
|
||||
# 复制依赖文件
|
||||
COPY ./web/package*.json ./
|
||||
COPY ./web/pnpm-lock.yaml* ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install --frozen-lockfile
|
||||
|
||||
# 复制源代码并构建
|
||||
COPY ./web .
|
||||
RUN npm run build
|
||||
RUN pnpm run build
|
||||
|
||||
# 生产环境运行阶段
|
||||
FROM nginx:alpine AS production
|
||||
|
||||
@ -3,9 +3,11 @@ from server.routers.chat_router import chat
|
||||
from server.routers.data_router import data
|
||||
from server.routers.base_router import base
|
||||
from server.routers.auth_router import auth
|
||||
from server.routers.graph_router import graph
|
||||
|
||||
router = APIRouter()
|
||||
router.include_router(base)
|
||||
router.include_router(chat)
|
||||
router.include_router(data)
|
||||
router.include_router(auth)
|
||||
router.include_router(graph)
|
||||
|
||||
258
server/routers/graph_router.py
Normal file
258
server/routers/graph_router.py
Normal file
@ -0,0 +1,258 @@
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import List, Dict, Any, Optional
|
||||
from fastapi import APIRouter, Query, HTTPException, Depends
|
||||
from server.utils.auth_middleware import get_admin_user
|
||||
from server.models.user_model import User
|
||||
|
||||
from src import knowledge_base
|
||||
from src.utils.logging_config import logger
|
||||
|
||||
graph = APIRouter()
|
||||
|
||||
|
||||
@graph.get("/graph/subgraph")
|
||||
async def get_subgraph(
|
||||
db_id: str = Query(..., description="数据库ID"),
|
||||
node_label: str = Query(..., description="节点标签或实体名称"),
|
||||
max_depth: int = Query(2, description="最大深度", ge=1, le=5),
|
||||
max_nodes: int = Query(100, description="最大节点数", ge=1, le=1000),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
使用 LightRAG 原生方法获取知识图谱子图
|
||||
|
||||
Args:
|
||||
db_id: LightRAG 数据库实例ID
|
||||
node_label: 节点标签,用于查找起始节点,使用 "*" 获取全图
|
||||
max_depth: 子图的最大深度
|
||||
max_nodes: 返回的最大节点数量
|
||||
|
||||
Returns:
|
||||
包含节点和边的知识图谱数据
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取子图数据 - db_id: {db_id}, node_label: {node_label}, max_depth: {max_depth}, max_nodes: {max_nodes}")
|
||||
|
||||
# 获取 LightRAG 实例
|
||||
rag_instance = await knowledge_base._get_lightrag_instance(db_id)
|
||||
if not rag_instance:
|
||||
raise HTTPException(status_code=404, detail=f"数据库 {db_id} 不存在")
|
||||
|
||||
# 使用 LightRAG 的原生 get_knowledge_graph 方法
|
||||
knowledge_graph = await rag_instance.get_knowledge_graph(
|
||||
node_label=node_label,
|
||||
max_depth=max_depth,
|
||||
max_nodes=max_nodes
|
||||
)
|
||||
|
||||
# 将 LightRAG 的 KnowledgeGraph 格式转换为前端需要的格式
|
||||
nodes = []
|
||||
for node in knowledge_graph.nodes:
|
||||
nodes.append({
|
||||
"id": node.id,
|
||||
"labels": node.labels,
|
||||
"entity_type": node.properties.get("entity_type", "unknown"),
|
||||
"properties": node.properties
|
||||
})
|
||||
|
||||
edges = []
|
||||
for edge in knowledge_graph.edges:
|
||||
edges.append({
|
||||
"id": edge.id,
|
||||
"source": edge.source,
|
||||
"target": edge.target,
|
||||
"type": edge.type,
|
||||
"properties": edge.properties
|
||||
})
|
||||
|
||||
result = {
|
||||
"success": True,
|
||||
"data": {
|
||||
"nodes": nodes,
|
||||
"edges": edges,
|
||||
"is_truncated": knowledge_graph.is_truncated,
|
||||
"total_nodes": len(nodes),
|
||||
"total_edges": len(edges)
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(f"成功获取子图 - 节点数: {len(nodes)}, 边数: {len(edges)}")
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取子图数据失败: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"获取子图数据失败: {str(e)}")
|
||||
|
||||
|
||||
@graph.get("/graph/labels")
|
||||
async def get_graph_labels(
|
||||
db_id: str = Query(..., description="数据库ID"),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
获取知识图谱中的所有标签
|
||||
|
||||
Args:
|
||||
db_id: LightRAG 数据库实例ID
|
||||
|
||||
Returns:
|
||||
图谱中所有可用的标签列表
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取图谱标签 - db_id: {db_id}")
|
||||
|
||||
# 获取 LightRAG 实例
|
||||
rag_instance = await knowledge_base._get_lightrag_instance(db_id)
|
||||
if not rag_instance:
|
||||
raise HTTPException(status_code=404, detail=f"数据库 {db_id} 不存在")
|
||||
|
||||
# 使用 LightRAG 的原生方法获取所有标签
|
||||
labels = await rag_instance.get_graph_labels()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"labels": labels
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取图谱标签失败: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"获取图谱标签失败: {str(e)}")
|
||||
|
||||
|
||||
@graph.get("/graph/databases")
|
||||
async def get_available_databases(
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
获取所有可用的 LightRAG 数据库
|
||||
|
||||
Returns:
|
||||
可用的数据库列表
|
||||
"""
|
||||
try:
|
||||
databases = knowledge_base.get_databases()
|
||||
return {
|
||||
"success": True,
|
||||
"data": databases
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取数据库列表失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取数据库列表失败: {str(e)}")
|
||||
|
||||
|
||||
# 保留原有的直接数据库查询方法作为备用(如果需要的话)
|
||||
@graph.get("/graph/nodes")
|
||||
async def get_graph_nodes_legacy(
|
||||
db_id: str = Query(..., description="数据库ID"),
|
||||
limit: int = Query(500, description="最大节点数量", ge=1, le=2000),
|
||||
offset: int = Query(0, description="偏移量", ge=0),
|
||||
entity_type: Optional[str] = Query(None, description="实体类型筛选"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
直接查询数据库获取节点数据(备用方法)
|
||||
建议使用 /graph/subgraph 接口
|
||||
"""
|
||||
try:
|
||||
# 这里可以添加直接数据库查询的逻辑
|
||||
# 但建议用户使用 get_subgraph 接口
|
||||
return {
|
||||
"success": False,
|
||||
"message": "建议使用 /graph/subgraph 接口获取图谱数据",
|
||||
"data": {
|
||||
"nodes": [],
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取图节点数据失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取图节点数据失败: {str(e)}")
|
||||
|
||||
|
||||
@graph.get("/graph/edges")
|
||||
async def get_graph_edges_legacy(
|
||||
db_id: str = Query(..., description="数据库ID"),
|
||||
limit: int = Query(500, description="最大边数量", ge=1, le=2000),
|
||||
offset: int = Query(0, description="偏移量", ge=0),
|
||||
min_weight: Optional[float] = Query(None, description="最小权重筛选"),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
直接查询数据库获取边数据(备用方法)
|
||||
建议使用 /graph/subgraph 接口
|
||||
"""
|
||||
try:
|
||||
# 这里可以添加直接数据库查询的逻辑
|
||||
# 但建议用户使用 get_subgraph 接口
|
||||
return {
|
||||
"success": False,
|
||||
"message": "建议使用 /graph/subgraph 接口获取图谱数据",
|
||||
"data": {
|
||||
"edges": [],
|
||||
"total": 0
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取图边数据失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"获取图边数据失败: {str(e)}")
|
||||
|
||||
|
||||
@graph.get("/graph/stats")
|
||||
async def get_graph_stats(
|
||||
db_id: str = Query(..., description="数据库ID"),
|
||||
current_user: User = Depends(get_admin_user)
|
||||
):
|
||||
"""
|
||||
获取知识图谱统计信息
|
||||
"""
|
||||
try:
|
||||
logger.info(f"获取图谱统计信息 - db_id: {db_id}")
|
||||
|
||||
# 获取 LightRAG 实例
|
||||
rag_instance = await knowledge_base._get_lightrag_instance(db_id)
|
||||
if not rag_instance:
|
||||
raise HTTPException(status_code=404, detail=f"数据库 {db_id} 不存在")
|
||||
|
||||
# 通过获取全图来统计节点和边的数量
|
||||
knowledge_graph = await rag_instance.get_knowledge_graph(
|
||||
node_label="*",
|
||||
max_depth=1,
|
||||
max_nodes=10000 # 设置较大值以获取完整统计
|
||||
)
|
||||
|
||||
# 统计实体类型分布
|
||||
entity_types = {}
|
||||
for node in knowledge_graph.nodes:
|
||||
entity_type = node.properties.get("entity_type", "unknown")
|
||||
entity_types[entity_type] = entity_types.get(entity_type, 0) + 1
|
||||
|
||||
entity_types_list = [
|
||||
{"type": k, "count": v}
|
||||
for k, v in sorted(entity_types.items(), key=lambda x: x[1], reverse=True)
|
||||
]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"total_nodes": len(knowledge_graph.nodes),
|
||||
"total_edges": len(knowledge_graph.edges),
|
||||
"entity_types": entity_types_list,
|
||||
"is_truncated": knowledge_graph.is_truncated
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取图谱统计信息失败: {e}")
|
||||
logger.error(f"Traceback: {traceback.format_exc()}")
|
||||
raise HTTPException(status_code=500, detail=f"获取图谱统计信息失败: {str(e)}")
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yuxi-know-frontend",
|
||||
"version": "0.0.0",
|
||||
"version": "0.2.0.dev",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -14,6 +14,8 @@
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^6.1.0",
|
||||
"@antv/g6": "^5.0.47",
|
||||
"@sigma/edge-curve": "^3.1.0",
|
||||
"@sigma/node-border": "^3.0.0",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.9.0",
|
||||
@ -21,6 +23,8 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"echarts": "^5.6.0",
|
||||
"echarts-gl": "^2.0.9",
|
||||
"graphology": "^0.26.0",
|
||||
"graphology-generators": "^0.11.2",
|
||||
"highlight.js": "^11.11.1",
|
||||
"less": "^4.3.0",
|
||||
"lucide-vue-next": "^0.511.0",
|
||||
@ -28,6 +32,7 @@
|
||||
"marked-highlight": "^2.2.1",
|
||||
"md-editor-v3": "^5.5.1",
|
||||
"pinia": "^3.0.2",
|
||||
"sigma": "^3.0.1",
|
||||
"vue": "^3.5.14",
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
|
||||
238
web/src/apis/graph_api.js
Normal file
238
web/src/apis/graph_api.js
Normal file
@ -0,0 +1,238 @@
|
||||
import { apiGet } from './base'
|
||||
|
||||
/**
|
||||
* 图数据API调用 - 基于LightRAG的新接口
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取所有可用的数据库
|
||||
* @returns {Promise} - 数据库列表
|
||||
*/
|
||||
export const getAvailableDatabases = async () => {
|
||||
return await apiGet('/api/graph/databases', {}, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图标签列表
|
||||
* @param {string} dbId - 数据库ID
|
||||
* @returns {Promise} - 标签列表
|
||||
*/
|
||||
export const getGraphLabels = async (dbId) => {
|
||||
if (!dbId) {
|
||||
throw new Error('db_id is required')
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
db_id: dbId
|
||||
})
|
||||
|
||||
return await apiGet(`/api/graph/labels?${queryParams.toString()}`, {}, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取子图数据 - 主要接口
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.db_id - 数据库ID
|
||||
* @param {string} params.node_label - 节点标签 (使用 "*" 获取全图)
|
||||
* @param {number} params.max_depth - 最大深度
|
||||
* @param {number} params.max_nodes - 最大节点数
|
||||
* @returns {Promise} - 子图数据
|
||||
*/
|
||||
export const getSubgraph = async (params) => {
|
||||
const { db_id, node_label = "*", max_depth = 2, max_nodes = 100 } = params
|
||||
|
||||
if (!db_id) {
|
||||
throw new Error('db_id is required')
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
db_id: db_id,
|
||||
node_label: node_label,
|
||||
max_depth: max_depth.toString(),
|
||||
max_nodes: max_nodes.toString()
|
||||
})
|
||||
|
||||
return await apiGet(`/api/graph/subgraph?${queryParams.toString()}`, {}, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图统计信息
|
||||
* @param {string} dbId - 数据库ID
|
||||
* @returns {Promise} - 统计数据
|
||||
*/
|
||||
export const getGraphStats = async (dbId) => {
|
||||
if (!dbId) {
|
||||
throw new Error('db_id is required')
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
db_id: dbId
|
||||
})
|
||||
|
||||
return await apiGet(`/api/graph/stats?${queryParams.toString()}`, {}, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取完整图数据 - 使用新的子图接口
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.db_id - 数据库ID
|
||||
* @param {string} params.node_label - 节点标签筛选
|
||||
* @param {number} params.max_nodes - 最大节点数
|
||||
* @param {number} params.max_depth - 最大深度
|
||||
* @returns {Promise} - 完整图数据
|
||||
*/
|
||||
export const getFullGraph = async (params = {}) => {
|
||||
const { db_id, node_label = "*", max_nodes = 200, max_depth = 2 } = params
|
||||
|
||||
if (!db_id) {
|
||||
throw new Error('db_id is required for graph operations')
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用子图接口获取数据
|
||||
const response = await getSubgraph({
|
||||
db_id,
|
||||
node_label,
|
||||
max_nodes,
|
||||
max_depth
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error('获取图数据失败')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
nodes: response.data.nodes,
|
||||
edges: response.data.edges,
|
||||
is_truncated: response.data.is_truncated,
|
||||
stats: {
|
||||
total_nodes: response.data.total_nodes,
|
||||
total_edges: response.data.total_edges,
|
||||
displayed_nodes: response.data.nodes.length,
|
||||
displayed_edges: response.data.edges.length
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取完整图数据失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据特定标签获取子图
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.db_id - 数据库ID
|
||||
* @param {string} params.entity_type - 实体类型
|
||||
* @param {number} params.max_nodes - 最大节点数
|
||||
* @param {number} params.max_depth - 最大深度
|
||||
* @returns {Promise} - 子图数据
|
||||
*/
|
||||
export const getGraphByEntityType = async (params = {}) => {
|
||||
const { db_id, entity_type, max_nodes = 100, max_depth = 2 } = params
|
||||
|
||||
if (!db_id) {
|
||||
throw new Error('db_id is required')
|
||||
}
|
||||
|
||||
return await getSubgraph({
|
||||
db_id,
|
||||
node_label: entity_type || "*",
|
||||
max_nodes,
|
||||
max_depth
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 展开指定节点的邻居
|
||||
* @param {Object} params - 查询参数
|
||||
* @param {string} params.db_id - 数据库ID
|
||||
* @param {string} params.node_label - 节点标签
|
||||
* @param {number} params.max_depth - 最大深度
|
||||
* @param {number} params.max_nodes - 最大节点数
|
||||
* @returns {Promise} - 邻居节点数据
|
||||
*/
|
||||
export const expandNodeNeighbors = async (params) => {
|
||||
const { db_id, node_label, max_depth = 1, max_nodes = 50 } = params
|
||||
|
||||
if (!db_id || !node_label) {
|
||||
throw new Error('db_id and node_label are required')
|
||||
}
|
||||
|
||||
return await getSubgraph({
|
||||
db_id,
|
||||
node_label,
|
||||
max_depth,
|
||||
max_nodes
|
||||
})
|
||||
}
|
||||
|
||||
// ==================== 兼容性方法 ====================
|
||||
|
||||
/**
|
||||
* 获取图节点数据 (已弃用,建议使用 getSubgraph)
|
||||
* @deprecated 请使用 getSubgraph 替代
|
||||
*/
|
||||
export const getGraphNodes = async (params = {}) => {
|
||||
console.warn('getGraphNodes is deprecated, please use getSubgraph instead')
|
||||
const { db_id } = params
|
||||
if (!db_id) {
|
||||
throw new Error('db_id is required. Please provide db_id parameter.')
|
||||
}
|
||||
return await getSubgraph({ ...params, node_label: "*" })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图边数据 (已弃用,建议使用 getSubgraph)
|
||||
* @deprecated 请使用 getSubgraph 替代
|
||||
*/
|
||||
export const getGraphEdges = async (params = {}) => {
|
||||
console.warn('getGraphEdges is deprecated, please use getSubgraph instead')
|
||||
const { db_id } = params
|
||||
if (!db_id) {
|
||||
throw new Error('db_id is required. Please provide db_id parameter.')
|
||||
}
|
||||
return await getSubgraph({ ...params, node_label: "*" })
|
||||
}
|
||||
|
||||
// ==================== 工具函数 ====================
|
||||
|
||||
/**
|
||||
* 根据实体类型获取颜色
|
||||
* @param {string} entityType - 实体类型
|
||||
* @returns {string} - 颜色值
|
||||
*/
|
||||
export const getEntityTypeColor = (entityType) => {
|
||||
const colorMap = {
|
||||
'person': '#FF6B6B', // 红色 - 人物
|
||||
'organization': '#4ECDC4', // 青色 - 组织
|
||||
'location': '#45B7D1', // 蓝色 - 地点
|
||||
'geo': '#45B7D1', // 蓝色 - 地理位置
|
||||
'event': '#96CEB4', // 绿色 - 事件
|
||||
'category': '#FFEAA7', // 黄色 - 分类
|
||||
'equipment': '#DDA0DD', // 紫色 - 设备
|
||||
'athlete': '#FF7675', // 红色 - 运动员
|
||||
'record': '#FD79A8', // 粉色 - 记录
|
||||
'year': '#FDCB6E', // 橙色 - 年份
|
||||
'UNKNOWN': '#B2BEC3', // 灰色 - 未知
|
||||
'unknown': '#B2BEC3' // 灰色 - 未知
|
||||
}
|
||||
|
||||
return colorMap[entityType] || colorMap['unknown']
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据权重计算边的粗细
|
||||
* @param {number} weight - 权重值
|
||||
* @param {number} minWeight - 最小权重
|
||||
* @param {number} maxWeight - 最大权重
|
||||
* @returns {number} - 边的粗细
|
||||
*/
|
||||
export const calculateEdgeWidth = (weight, minWeight = 1, maxWeight = 10) => {
|
||||
const minWidth = 1
|
||||
const maxWidth = 5
|
||||
const normalizedWeight = (weight - minWeight) / (maxWeight - minWeight)
|
||||
return minWidth + normalizedWeight * (maxWidth - minWidth)
|
||||
}
|
||||
@ -1,4 +1,6 @@
|
||||
@import './base.css';
|
||||
@import './sigma.css';
|
||||
@import './shorts.css';
|
||||
|
||||
:root {
|
||||
--header-height: 60px;
|
||||
46
web/src/assets/css/shorts.css
Normal file
46
web/src/assets/css/shorts.css
Normal file
@ -0,0 +1,46 @@
|
||||
.m-2 {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.mt-2 {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ml-2 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
|
||||
.m-3 {
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
.mt-3 {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.mb-3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ml-3 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.mr-3 {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
|
||||
.bn-1px {
|
||||
position: relative;
|
||||
bottom: -1px;
|
||||
}
|
||||
88
web/src/assets/css/sigma.css
Normal file
88
web/src/assets/css/sigma.css
Normal file
@ -0,0 +1,88 @@
|
||||
/* Sigma.js 基础样式 */
|
||||
.sigma-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.sigma-container canvas {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* 节点标签样式 */
|
||||
.sigma-labels {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 高亮样式 */
|
||||
.node-highlighted {
|
||||
stroke: #ff0000 !important;
|
||||
stroke-width: 3px !important;
|
||||
}
|
||||
|
||||
.edge-highlighted {
|
||||
stroke: #ff0000 !important;
|
||||
stroke-width: 3px !important;
|
||||
}
|
||||
|
||||
/* 选中样式 */
|
||||
.node-selected {
|
||||
stroke: #1890ff !important;
|
||||
stroke-width: 4px !important;
|
||||
}
|
||||
|
||||
.edge-selected {
|
||||
stroke: #1890ff !important;
|
||||
stroke-width: 4px !important;
|
||||
}
|
||||
|
||||
/* 悬停样式 */
|
||||
.node-focused {
|
||||
stroke: #52c41a !important;
|
||||
stroke-width: 2px !important;
|
||||
}
|
||||
|
||||
.edge-focused {
|
||||
stroke: #52c41a !important;
|
||||
stroke-width: 2px !important;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.sigma-loading {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
/* 工具提示 */
|
||||
.sigma-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
z-index: 1000;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.sigma-container {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.sigma-tooltip {
|
||||
font-size: 10px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
}
|
||||
@ -1317,7 +1317,7 @@ const mergeMessageChunk = (chunks) => {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '@/assets/main.css';
|
||||
@import '@/assets/css/main.css';
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
<template>
|
||||
<div class="header-container">
|
||||
<div class="header-content">
|
||||
<div class="header-actions" v-if="$slots.left">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
<div class="header-title">
|
||||
<h1>{{ title }}</h1>
|
||||
<slot name="description">
|
||||
@ -48,9 +51,12 @@ const props = defineProps({
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
|
||||
|
||||
1386
web/src/components/KnowledgeGraphViewer.vue
Normal file
1386
web/src/components/KnowledgeGraphViewer.vue
Normal file
File diff suppressed because it is too large
Load Diff
@ -207,7 +207,7 @@ const mainList = [{
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import '@/assets/main.css';
|
||||
@import '@/assets/css/main.css';
|
||||
|
||||
:root {
|
||||
--header-width: 60px;
|
||||
|
||||
@ -6,7 +6,7 @@ import router from './router'
|
||||
|
||||
import Antd from 'ant-design-vue';
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
import './assets/main.css'
|
||||
import '@/assets/css/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
397
web/src/stores/graphStore.js
Normal file
397
web/src/stores/graphStore.js
Normal file
@ -0,0 +1,397 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { DirectedGraph } from 'graphology'
|
||||
|
||||
export const useGraphStore = defineStore('graph', {
|
||||
state: () => ({
|
||||
// 选中和聚焦状态
|
||||
selectedNode: null,
|
||||
focusedNode: null,
|
||||
selectedEdge: null,
|
||||
focusedEdge: null,
|
||||
|
||||
// 图数据
|
||||
rawGraph: null,
|
||||
sigmaGraph: null,
|
||||
sigmaInstance: null,
|
||||
|
||||
// 实体类型和颜色映射
|
||||
entityTypes: [],
|
||||
typeColorMap: new Map(),
|
||||
|
||||
// 加载状态
|
||||
isFetching: false,
|
||||
graphIsEmpty: false,
|
||||
|
||||
// 移动到选中节点的标志
|
||||
moveToSelectedNode: false,
|
||||
|
||||
// 图统计信息
|
||||
stats: {
|
||||
total_nodes: 0,
|
||||
total_edges: 0,
|
||||
displayed_nodes: 0,
|
||||
displayed_edges: 0
|
||||
}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
// 获取选中节点的详细信息
|
||||
selectedNodeData: (state) => {
|
||||
if (!state.selectedNode || !state.rawGraph) return null
|
||||
return state.rawGraph.nodes.find(node => node.id === state.selectedNode)
|
||||
},
|
||||
|
||||
// 获取选中边的详细信息
|
||||
selectedEdgeData: (state) => {
|
||||
if (!state.selectedEdge || !state.rawGraph) return null
|
||||
// 首先尝试通过dynamicId匹配
|
||||
let foundEdge = state.rawGraph.edges.find(edge => edge.dynamicId === state.selectedEdge)
|
||||
if (foundEdge) return foundEdge
|
||||
|
||||
// 如果没找到,尝试通过原始ID匹配
|
||||
foundEdge = state.rawGraph.edges.find(edge => edge.id === state.selectedEdge)
|
||||
if (foundEdge) return foundEdge
|
||||
|
||||
// 最后尝试通过source->target格式匹配
|
||||
const [source, target] = state.selectedEdge.split('->')
|
||||
if (source && target) {
|
||||
foundEdge = state.rawGraph.edges.find(edge =>
|
||||
edge.source === source.trim() && edge.target === target.trim()
|
||||
)
|
||||
}
|
||||
|
||||
return foundEdge || null
|
||||
},
|
||||
|
||||
// 检查图是否为空
|
||||
isGraphEmpty: (state) => {
|
||||
return !state.rawGraph || state.rawGraph.nodes.length === 0
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 设置Sigma实例
|
||||
setSigmaInstance(instance) {
|
||||
this.sigmaInstance = instance
|
||||
},
|
||||
|
||||
// 节点选择和聚焦
|
||||
setSelectedNode(nodeId, moveToNode = false) {
|
||||
this.selectedNode = nodeId
|
||||
this.moveToSelectedNode = moveToNode
|
||||
// 如果选中节点,清除选中的边
|
||||
if (nodeId) {
|
||||
this.selectedEdge = null
|
||||
}
|
||||
},
|
||||
|
||||
setFocusedNode(nodeId) {
|
||||
this.focusedNode = nodeId
|
||||
},
|
||||
|
||||
setSelectedEdge(edgeId) {
|
||||
this.selectedEdge = edgeId
|
||||
// 如果选中边,清除选中的节点
|
||||
if (edgeId) {
|
||||
this.selectedNode = null
|
||||
}
|
||||
},
|
||||
|
||||
setFocusedEdge(edgeId) {
|
||||
this.focusedEdge = edgeId
|
||||
},
|
||||
|
||||
// 清除所有选择
|
||||
clearSelection() {
|
||||
this.selectedNode = null
|
||||
this.focusedNode = null
|
||||
this.selectedEdge = null
|
||||
this.focusedEdge = null
|
||||
},
|
||||
|
||||
// 设置加载状态
|
||||
setIsFetching(isFetching) {
|
||||
this.isFetching = isFetching
|
||||
},
|
||||
|
||||
// 设置实体类型
|
||||
setEntityTypes(types) {
|
||||
this.entityTypes = types
|
||||
this.updateTypeColorMap()
|
||||
},
|
||||
|
||||
// 更新类型颜色映射
|
||||
updateTypeColorMap() {
|
||||
const colorPalette = [
|
||||
'#FF6B6B', // 人物 - 红色
|
||||
'#4ECDC4', // 组织 - 青色
|
||||
'#45B7D1', // 地点 - 蓝色
|
||||
'#96CEB4', // 事件 - 绿色
|
||||
'#FFEAA7', // 分类 - 黄色
|
||||
'#DDA0DD', // 设备 - 紫色
|
||||
'#FF7675', // 运动员 - 红色
|
||||
'#FD79A8', // 记录 - 粉色
|
||||
'#FDCB6E', // 年份 - 橙色
|
||||
'#B2BEC3' // 未知 - 灰色
|
||||
]
|
||||
|
||||
const typeColorMap = new Map()
|
||||
this.entityTypes.forEach((type, index) => {
|
||||
const colorIndex = index % colorPalette.length
|
||||
typeColorMap.set(type.type, colorPalette[colorIndex])
|
||||
})
|
||||
|
||||
// 为特殊类型设置固定颜色
|
||||
typeColorMap.set('person', '#FF6B6B')
|
||||
typeColorMap.set('organization', '#4ECDC4')
|
||||
typeColorMap.set('location', '#45B7D1')
|
||||
typeColorMap.set('geo', '#45B7D1')
|
||||
typeColorMap.set('event', '#96CEB4')
|
||||
typeColorMap.set('category', '#FFEAA7')
|
||||
typeColorMap.set('unknown', '#B2BEC3')
|
||||
|
||||
this.typeColorMap = typeColorMap
|
||||
},
|
||||
|
||||
// 获取实体类型颜色
|
||||
getEntityColor(entityType) {
|
||||
return this.typeColorMap.get(entityType) || '#B2BEC3'
|
||||
},
|
||||
|
||||
// 设置原始图数据
|
||||
setRawGraph(rawGraph) {
|
||||
this.rawGraph = rawGraph
|
||||
this.graphIsEmpty = !rawGraph || rawGraph.nodes.length === 0
|
||||
this.updateStats()
|
||||
},
|
||||
|
||||
// 设置Sigma图数据
|
||||
setSigmaGraph(sigmaGraph) {
|
||||
this.sigmaGraph = sigmaGraph
|
||||
},
|
||||
|
||||
// 更新统计信息
|
||||
updateStats() {
|
||||
if (this.rawGraph) {
|
||||
this.stats = {
|
||||
total_nodes: this.rawGraph.nodes.length,
|
||||
total_edges: this.rawGraph.edges.length,
|
||||
displayed_nodes: this.rawGraph.nodes.length,
|
||||
displayed_edges: this.rawGraph.edges.length
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 从API数据创建图数据结构
|
||||
createGraphFromApiData(nodesData, edgesData) {
|
||||
const rawGraph = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
nodeIdMap: {},
|
||||
edgeIdMap: {},
|
||||
edgeDynamicIdMap: {}
|
||||
}
|
||||
|
||||
console.log('Processing nodes data:', nodesData)
|
||||
|
||||
// 处理节点数据
|
||||
nodesData.forEach((node, index) => {
|
||||
// 适配新的LightRAG API格式
|
||||
const nodeId = String(node.id)
|
||||
const labels = node.labels || [node.entity_type || 'unknown']
|
||||
const entityType = node.entity_type || labels[0] || 'unknown'
|
||||
|
||||
const processedNode = {
|
||||
id: nodeId,
|
||||
labels: Array.isArray(labels) ? labels.map(String) : [String(labels)],
|
||||
entity_type: String(entityType),
|
||||
properties: {
|
||||
entity_id: String(node.properties?.entity_id || node.entity_id || nodeId),
|
||||
entity_type: String(entityType),
|
||||
description: String(node.properties?.description || node.description || ''),
|
||||
file_path: String(node.properties?.file_path || node.file_path || ''),
|
||||
...(node.properties || {})
|
||||
},
|
||||
// Sigma.js需要的属性
|
||||
size: this.calculateNodeSize(node),
|
||||
x: Math.random() * 1000, // 随机初始位置
|
||||
y: Math.random() * 1000,
|
||||
color: this.getEntityColor(String(entityType)),
|
||||
degree: 0 // 将在处理边时计算
|
||||
}
|
||||
|
||||
rawGraph.nodes.push(processedNode)
|
||||
rawGraph.nodeIdMap[nodeId] = index
|
||||
})
|
||||
|
||||
// 计算节点度数
|
||||
const nodeDegrees = {}
|
||||
edgesData.forEach(edge => {
|
||||
const sourceId = String(edge.source)
|
||||
const targetId = String(edge.target)
|
||||
nodeDegrees[sourceId] = (nodeDegrees[sourceId] || 0) + 1
|
||||
nodeDegrees[targetId] = (nodeDegrees[targetId] || 0) + 1
|
||||
})
|
||||
|
||||
// 更新节点度数和大小
|
||||
rawGraph.nodes.forEach(node => {
|
||||
node.degree = nodeDegrees[node.id] || 0
|
||||
node.size = this.calculateNodeSize({ degree: node.degree })
|
||||
})
|
||||
|
||||
console.log('Processing edges data:', edgesData)
|
||||
|
||||
// 处理边数据
|
||||
edgesData.forEach((edge, index) => {
|
||||
const sourceId = String(edge.source)
|
||||
const targetId = String(edge.target)
|
||||
const dynamicId = `${sourceId}-${targetId}-${index}`
|
||||
|
||||
// 适配新的LightRAG API格式
|
||||
const weight = Number(edge.properties?.weight || edge.weight || 1.0)
|
||||
|
||||
const processedEdge = {
|
||||
id: String(edge.id),
|
||||
source: sourceId,
|
||||
target: targetId,
|
||||
type: edge.type || 'DIRECTED',
|
||||
properties: {
|
||||
weight: weight,
|
||||
keywords: String(edge.properties?.keywords || edge.keywords || ''),
|
||||
description: String(edge.properties?.description || edge.description || ''),
|
||||
file_path: String(edge.properties?.file_path || edge.file_path || ''),
|
||||
...(edge.properties || {})
|
||||
},
|
||||
dynamicId: dynamicId,
|
||||
// Sigma.js需要的属性
|
||||
size: this.calculateEdgeSize(weight),
|
||||
color: '#666',
|
||||
originalWeight: weight
|
||||
}
|
||||
|
||||
rawGraph.edges.push(processedEdge)
|
||||
rawGraph.edgeIdMap[edge.id] = index
|
||||
rawGraph.edgeDynamicIdMap[dynamicId] = index
|
||||
})
|
||||
|
||||
return rawGraph
|
||||
},
|
||||
|
||||
// 计算节点大小
|
||||
calculateNodeSize(node) {
|
||||
const baseSizeM = 15
|
||||
const degree = node.degree || 0
|
||||
return Math.min(baseSizeM + degree * 3, 40)
|
||||
},
|
||||
|
||||
// 计算边大小
|
||||
calculateEdgeSize(weight) {
|
||||
const minSize = 3 // 与Sigma配置中的minEdgeThickness保持一致
|
||||
const maxSize = 8 // 与Sigma配置中的maxEdgeThickness保持一致
|
||||
const normalizedWeight = Math.max(0, Math.min(1, (weight - 1) / 9)) // 假设权重范围1-10
|
||||
return minSize + normalizedWeight * (maxSize - minSize)
|
||||
},
|
||||
|
||||
// 创建Sigma图实例
|
||||
createSigmaGraph(rawGraph) {
|
||||
console.log('开始创建Sigma图,节点数量:', rawGraph.nodes.length, '边数量:', rawGraph.edges.length)
|
||||
const sigmaGraph = new DirectedGraph()
|
||||
|
||||
// 添加节点
|
||||
rawGraph.nodes.forEach(node => {
|
||||
// 确保所有属性都是正确的类型
|
||||
const nodeAttributes = {
|
||||
label: String(node.properties?.entity_id || node.id),
|
||||
size: Number(node.size) || 15,
|
||||
color: String(node.color) || '#B2BEC3',
|
||||
x: Number(node.x) || Math.random() * 1000,
|
||||
y: Number(node.y) || Math.random() * 1000,
|
||||
// 保存原始数据引用
|
||||
originalData: node
|
||||
}
|
||||
|
||||
sigmaGraph.addNode(String(node.id), nodeAttributes)
|
||||
})
|
||||
|
||||
console.log('节点添加完成,开始添加边...')
|
||||
|
||||
// 添加边
|
||||
let edgeAddedCount = 0
|
||||
let edgeSkippedCount = 0
|
||||
rawGraph.edges.forEach((edge, index) => {
|
||||
// 添加调试信息
|
||||
if (index < 3) {
|
||||
console.log('处理边 #' + index + ':', {
|
||||
id: edge.id,
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
dynamicId: edge.dynamicId,
|
||||
edgeObject: edge
|
||||
})
|
||||
}
|
||||
|
||||
if (sigmaGraph.hasNode(String(edge.source)) && sigmaGraph.hasNode(String(edge.target))) {
|
||||
// 确保所有属性都是正确的类型
|
||||
const edgeAttributes = {
|
||||
size: Number(edge.size) || 1,
|
||||
color: String(edge.color) || '#666',
|
||||
label: String(edge.properties?.keywords || edge.properties?.description || ''),
|
||||
originalWeight: Number(edge.originalWeight) || 1,
|
||||
// 保存原始数据引用
|
||||
originalData: edge
|
||||
}
|
||||
|
||||
// 使用标准的 addEdge 方法:addEdge(edgeId, source, target, attributes)
|
||||
try {
|
||||
// 使用动态ID作为Sigma边ID,避免重复
|
||||
const sigmaEdgeId = edge.dynamicId || `${edge.source}->${edge.target}`
|
||||
|
||||
// 检查是否已存在相同的边
|
||||
if (!sigmaGraph.hasEdge(sigmaEdgeId)) {
|
||||
sigmaGraph.addEdgeWithKey(sigmaEdgeId, String(edge.source), String(edge.target), edgeAttributes)
|
||||
edgeAddedCount++
|
||||
} else {
|
||||
edgeSkippedCount++
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('添加边失败:', {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
attributes: edgeAttributes,
|
||||
error: err.message
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.warn('节点不存在,跳过边:', {
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
hasSource: sigmaGraph.hasNode(String(edge.source)),
|
||||
hasTarget: sigmaGraph.hasNode(String(edge.target))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
console.log(`边添加完成: 成功 ${edgeAddedCount}, 跳过 ${edgeSkippedCount}`)
|
||||
|
||||
return sigmaGraph
|
||||
},
|
||||
|
||||
// 重置所有状态
|
||||
reset() {
|
||||
this.selectedNode = null
|
||||
this.focusedNode = null
|
||||
this.selectedEdge = null
|
||||
this.focusedEdge = null
|
||||
this.rawGraph = null
|
||||
this.sigmaGraph = null
|
||||
this.moveToSelectedNode = false
|
||||
this.graphIsEmpty = false
|
||||
this.stats = {
|
||||
total_nodes: 0,
|
||||
total_edges: 0,
|
||||
displayed_nodes: 0,
|
||||
displayed_edges: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -4,18 +4,13 @@
|
||||
:title="database.name || '数据库信息'"
|
||||
:loading="state.databaseLoading"
|
||||
>
|
||||
<template #description>
|
||||
<div class="database-info">
|
||||
<a-tag color="blue" v-if="database.embed_model">{{ database.embed_model }}</a-tag>
|
||||
<a-tag color="green" v-if="database.dimension">{{ database.dimension }}</a-tag>
|
||||
<span class="row-count">{{ database.files ? Object.keys(database.files).length : 0 }} 文件 · {{ database.db_id }}</span>
|
||||
</div>
|
||||
<template #left>
|
||||
<a-button type="text" @click="backToDatabase">
|
||||
<LeftOutlined />
|
||||
</a-button>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" @click="backToDatabase">
|
||||
<LeftOutlined /> 返回
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showEditModal">
|
||||
<a-button type="text" @click="showEditModal">
|
||||
<EditOutlined />
|
||||
</a-button>
|
||||
</template>
|
||||
@ -172,6 +167,12 @@
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 数据库信息 -->
|
||||
<div class="database-info">
|
||||
<a-tag color="blue" v-if="database.embed_model">{{ database.embed_model }}</a-tag>
|
||||
<a-tag color="green" v-if="database.dimension">{{ database.dimension }}</a-tag>
|
||||
<span class="row-count">{{ database.files ? Object.keys(database.files).length : 0 }} 文件 · {{ database.db_id }}</span>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
@ -310,6 +311,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="knowledge-graph" force-render>
|
||||
<template #tab><span><Waypoints size="14" class="mr-3 bn-1px" />知识图谱(开发中)</span></template>
|
||||
<div class="knowledge-graph-container db-tab-container">
|
||||
<KnowledgeGraphViewer
|
||||
:initial-database-id="databaseId"
|
||||
:hide-db-selector="true"
|
||||
/>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- <a-tab-pane key="3" tab="Tab 3">Content of Tab Pane 3</a-tab-pane> -->
|
||||
<template #rightExtra>
|
||||
<div class="auto-refresh-control">
|
||||
@ -323,13 +335,12 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref, watch, toRaw, onUnmounted, computed } from 'vue';
|
||||
import { onMounted, reactive, ref, watch, toRaw, onUnmounted, computed, h } from 'vue';
|
||||
import { message, Modal } from 'ant-design-vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { knowledgeBaseApi } from '@/apis/admin_api'
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
||||
import {
|
||||
ReadOutlined,
|
||||
LeftOutlined,
|
||||
@ -346,8 +357,12 @@ import {
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
SettingOutlined,
|
||||
ApartmentOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import { h } from 'vue';
|
||||
import HeaderComponent from '@/components/HeaderComponent.vue';
|
||||
import KnowledgeGraphViewer from '@/components/KnowledgeGraphViewer.vue';
|
||||
import { Waypoints } from 'lucide-vue-next';
|
||||
|
||||
|
||||
|
||||
const route = useRoute();
|
||||
@ -1026,6 +1041,11 @@ const toggleAutoRefresh = (checked) => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.knowledge-graph-container {
|
||||
height: calc(100vh - 150px);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.query-test-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -1628,6 +1648,17 @@ const toggleAutoRefresh = (checked) => {
|
||||
}
|
||||
}
|
||||
|
||||
.knowledge-graph-container {
|
||||
height: calc(100vh - 200px);
|
||||
|
||||
:deep(.knowledge-graph-viewer) {
|
||||
height: 100%;
|
||||
|
||||
.sigma-container {
|
||||
height: calc(100% - 80px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user