Compare commits

...

2 Commits

Author SHA1 Message Date
guangfei.zhao
91eaa37283 refactor: update grid component props and optimize docker setup 2025-10-20 10:34:38 +08:00
guangfei.zhao
3f85b0ff78 feat(knowledge): add knowledge graph visualization component
- Add @xyflow/react dependency for graph visualization
- Create KnowledgeGraphView component with custom nodes and edges
- Extend knowledge detail hook to fetch and display graph data
- Add tabs in knowledge detail page to switch between documents and graph views
2025-10-17 16:43:03 +08:00
19 changed files with 959 additions and 212 deletions

98
.dockerignore Normal file
View File

@@ -0,0 +1,98 @@
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Git
.git
.gitignore
# Docker
Dockerfile*
docker-compose*
.dockerignore
# Environment files (keep only production)
.env
.env.local
.env.development.local
.env.test.local
# Logs
logs
*.log
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Dependency directories
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
# Storybook build outputs
.out
.storybook-out
# Temporary folders
tmp/
temp/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Stores VSCode versions used for testing VSCode extensions
.vscode-test

6
.env.production Normal file
View File

@@ -0,0 +1,6 @@
# VITE_API_BASE_URL = http://150.158.121.95
VITE_API_BASE_URL = http://154.9.253.114:9380
VITE_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB
-----END PUBLIC KEY-----"

View File

@@ -1,12 +1,60 @@
FROM node:20-alpine
# 多阶段构建 - 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
# 复制包管理文件
COPY package.json pnpm-lock.yaml ./
# 安装 pnpm 和依赖
RUN npm install -g pnpm && pnpm install
# 复制源代码
COPY . .
# 构建生产版本
RUN pnpm build
# 生产阶段 - nginx
FROM nginx:alpine AS production
# 复制自定义 nginx 配置
COPY <<EOF /etc/nginx/conf.d/default.conf
server {
listen 5173;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# 处理 SPA 路由
location / {
try_files \$uri \$uri/ /index.html;
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
}
EOF
# 从构建阶段复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
# 暴露端口
EXPOSE 5173
CMD ["pnpm", "dev"]
# 启动 nginx
CMD ["nginx", "-g", "daemon off;"]

28
docker-compose.yml Normal file
View File

@@ -0,0 +1,28 @@
version: '3.8'
services:
teres-frontend:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "3000:80"
environment:
- NODE_ENV=production
restart: unless-stopped
# 开发环境服务(可选)
teres-frontend-dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "5173:5173"
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
profiles:
- dev

View File

@@ -16,6 +16,7 @@
"@mui/material": "^7.3.4",
"@mui/x-data-grid": "^8.14.0",
"@mui/x-date-pickers": "^8.14.0",
"@xyflow/react": "^12.8.6",
"ahooks": "^3.9.5",
"axios": "^1.12.2",
"dayjs": "^1.11.18",

236
pnpm-lock.yaml generated
View File

@@ -26,6 +26,9 @@ importers:
'@mui/x-date-pickers':
specifier: ^8.14.0
version: 8.14.0(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@mui/material@7.3.4(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@mui/system@7.3.3(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(react@18.3.1))(@types/react@19.2.2)(dayjs@1.11.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@xyflow/react':
specifier: ^12.8.6
version: 12.8.6(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
ahooks:
specifier: ^3.9.5
version: 3.9.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -92,7 +95,7 @@ importers:
version: 19.2.1(@types/react@19.2.2)
'@vitejs/plugin-react':
specifier: ^5.0.4
version: 5.0.4(vite@7.1.9(@types/node@24.7.1))
version: 5.0.4(vite@7.1.9(@types/node@24.7.1)(terser@5.44.0))
eslint:
specifier: ^9.36.0
version: 9.37.0
@@ -113,7 +116,7 @@ importers:
version: 8.46.0(eslint@9.37.0)(typescript@5.9.3)
vite:
specifier: ^7.1.7
version: 7.1.9(@types/node@24.7.1)
version: 7.1.9(@types/node@24.7.1)(terser@5.44.0)
packages:
@@ -478,6 +481,9 @@ packages:
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/source-map@0.3.11':
resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
@@ -792,6 +798,24 @@ packages:
'@types/babel__traverse@7.28.0':
resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
'@types/d3-color@3.1.3':
resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
'@types/d3-drag@3.0.7':
resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
'@types/d3-interpolate@3.0.4':
resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
'@types/d3-selection@3.0.11':
resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
'@types/d3-transition@3.0.9':
resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
'@types/d3-zoom@3.0.8':
resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
'@types/estree@1.0.8':
resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
@@ -891,6 +915,15 @@ packages:
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
'@xyflow/react@12.8.6':
resolution: {integrity: sha512-SksAm2m4ySupjChphMmzvm55djtgMDPr+eovPDdTnyGvShf73cvydfoBfWDFllooIQ4IaiUL5yfxHRwU0c37EA==}
peerDependencies:
react: '>=17'
react-dom: '>=17'
'@xyflow/system@0.0.70':
resolution: {integrity: sha512-PpC//u9zxdjj0tfTSmZrg3+sRbTz6kop/Amky44U2Dl51sxzDTIUfXMwETOYpmr2dqICWXBIJwXL2a9QWtX2XA==}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@@ -950,6 +983,9 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@@ -965,6 +1001,9 @@ packages:
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
engines: {node: '>=10'}
classcat@5.0.5:
resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@@ -980,6 +1019,9 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1004,6 +1046,44 @@ packages:
csstype@3.1.3:
resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
dayjs@1.11.18:
resolution: {integrity: sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==}
@@ -1601,10 +1681,17 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map-support@0.5.21:
resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
source-map@0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'}
source-map@0.6.1:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@@ -1620,6 +1707,11 @@ packages:
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
engines: {node: '>= 0.4'}
terser@5.44.0:
resolution: {integrity: sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==}
engines: {node: '>=10'}
hasBin: true
tinyglobby@0.2.15:
resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
engines: {node: '>=12.0.0'}
@@ -1738,6 +1830,21 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zustand@4.5.7:
resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
engines: {node: '>=12.7.0'}
peerDependencies:
'@types/react': '>=16.8'
immer: '>=9.0.6'
react: '>=16.8'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
zustand@5.0.8:
resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==}
engines: {node: '>=12.20.0'}
@@ -2102,6 +2209,12 @@ snapshots:
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/source-map@0.3.11':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
optional: true
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
@@ -2358,6 +2471,27 @@ snapshots:
dependencies:
'@babel/types': 7.28.4
'@types/d3-color@3.1.3': {}
'@types/d3-drag@3.0.7':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-interpolate@3.0.4':
dependencies:
'@types/d3-color': 3.1.3
'@types/d3-selection@3.0.11': {}
'@types/d3-transition@3.0.9':
dependencies:
'@types/d3-selection': 3.0.11
'@types/d3-zoom@3.0.8':
dependencies:
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/estree@1.0.8': {}
'@types/js-cookie@3.0.6': {}
@@ -2479,7 +2613,7 @@ snapshots:
'@typescript-eslint/types': 8.46.0
eslint-visitor-keys: 4.2.1
'@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@24.7.1))':
'@vitejs/plugin-react@5.0.4(vite@7.1.9(@types/node@24.7.1)(terser@5.44.0))':
dependencies:
'@babel/core': 7.28.4
'@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4)
@@ -2487,10 +2621,33 @@ snapshots:
'@rolldown/pluginutils': 1.0.0-beta.38
'@types/babel__core': 7.20.5
react-refresh: 0.17.0
vite: 7.1.9(@types/node@24.7.1)
vite: 7.1.9(@types/node@24.7.1)(terser@5.44.0)
transitivePeerDependencies:
- supports-color
'@xyflow/react@12.8.6(@types/react@19.2.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@xyflow/system': 0.0.70
classcat: 5.0.5
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
zustand: 4.5.7(@types/react@19.2.2)(react@18.3.1)
transitivePeerDependencies:
- '@types/react'
- immer
'@xyflow/system@0.0.70':
dependencies:
'@types/d3-drag': 3.0.7
'@types/d3-interpolate': 3.0.4
'@types/d3-selection': 3.0.11
'@types/d3-transition': 3.0.9
'@types/d3-zoom': 3.0.8
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
acorn: 8.15.0
@@ -2566,6 +2723,9 @@ snapshots:
node-releases: 2.0.23
update-browserslist-db: 1.1.3(browserslist@4.26.3)
buffer-from@1.1.2:
optional: true
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@@ -2580,6 +2740,8 @@ snapshots:
ansi-styles: 4.3.0
supports-color: 7.2.0
classcat@5.0.5: {}
clsx@2.1.1: {}
color-convert@2.0.1:
@@ -2592,6 +2754,9 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@2.20.3:
optional: true
concat-map@0.0.1: {}
convert-source-map@1.9.0: {}
@@ -2616,6 +2781,42 @@ snapshots:
csstype@3.1.3: {}
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-selection@3.0.0: {}
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dayjs@1.11.18: {}
debug@4.4.3:
@@ -3189,8 +3390,17 @@ snapshots:
source-map-js@1.2.1: {}
source-map-support@0.5.21:
dependencies:
buffer-from: 1.1.2
source-map: 0.6.1
optional: true
source-map@0.5.7: {}
source-map@0.6.1:
optional: true
strip-json-comments@3.1.1: {}
stylis@4.2.0: {}
@@ -3201,6 +3411,14 @@ snapshots:
supports-preserve-symlinks-flag@1.0.0: {}
terser@5.44.0:
dependencies:
'@jridgewell/source-map': 0.3.11
acorn: 8.15.0
commander: 2.20.3
source-map-support: 0.5.21
optional: true
tinyglobby@0.2.15:
dependencies:
fdir: 6.5.0(picomatch@4.0.3)
@@ -3251,7 +3469,7 @@ snapshots:
uuid@13.0.0: {}
vite@7.1.9(@types/node@24.7.1):
vite@7.1.9(@types/node@24.7.1)(terser@5.44.0):
dependencies:
esbuild: 0.25.10
fdir: 6.5.0(picomatch@4.0.3)
@@ -3262,6 +3480,7 @@ snapshots:
optionalDependencies:
'@types/node': 24.7.1
fsevents: 2.3.3
terser: 5.44.0
void-elements@3.1.0: {}
@@ -3277,6 +3496,13 @@ snapshots:
yocto-queue@0.1.0: {}
zustand@4.5.7(@types/react@19.2.2)(react@18.3.1):
dependencies:
use-sync-external-store: 1.6.0(react@18.3.1)
optionalDependencies:
'@types/react': 19.2.2
react: 18.3.1
zustand@5.0.8(@types/react@19.2.2)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)):
optionalDependencies:
'@types/react': 19.2.2

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, useCallback } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import knowledgeService from '@/services/knowledge_service';
import type { IKnowledge, IKnowledgeResult, IParserConfig } from '@/interfaces/database/knowledge';
import type { IKnowledge, IKnowledgeGraph, IKnowledgeResult } from '@/interfaces/database/knowledge';
import type { IFetchKnowledgeListRequestParams } from '@/interfaces/request/knowledge';
/**
@@ -200,6 +200,12 @@ export const useKnowledgeDetail = (kbId: string) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [knowledgeGraph, setKnowledgeGraph] = useState<IKnowledgeGraph | null>(null);
const showKnowledgeGraph = useMemo(() => {
return knowledgeGraph !== null && Object.keys(knowledgeGraph?.graph || {}).length > 0;
}, [knowledgeGraph]);
const fetchKnowledgeDetail = useCallback(async () => {
if (!kbId) return;
@@ -223,15 +229,41 @@ export const useKnowledgeDetail = (kbId: string) => {
}
}, [kbId]);
const fetchKnowledgeGraph = useCallback(async () => {
if (!kbId) return;
try {
setLoading(true);
setError(null);
const response = await knowledgeService.getKnowledgeGraph(kbId);
if (response.data.code === 0) {
setKnowledgeGraph(response.data.data);
} else {
throw new Error(response.data.message || '获取知识库图失败');
}
} catch (err: any) {
const errorMessage = err.response?.data?.message || err.message || '获取知识库图失败';
setError(errorMessage);
console.error('Failed to fetch knowledge graph:', err);
} finally {
setLoading(false);
}
}, [kbId]);
useEffect(() => {
fetchKnowledgeDetail();
}, [fetchKnowledgeDetail]);
fetchKnowledgeGraph();
}, [fetchKnowledgeDetail, fetchKnowledgeGraph]);
return {
knowledge,
knowledgeGraph,
loading,
error,
refresh: fetchKnowledgeDetail,
showKnowledgeGraph,
};
};

View File

@@ -1,3 +1,7 @@
// import type { AgentCategory } from '@/constants/agent';
import type { Edge, Node } from '@xyflow/react';
import type { IReference, Message } from './chat';
export interface ICategorizeItem {
name: string;
description?: string;
@@ -30,11 +34,6 @@ export interface ISwitchForm {
no: string;
}
import type { AgentCategory } from '@/constants/agent';
import type { Edge, Node } from '@xyflow/react';
import type { IReference, Message } from './chat';
export type DSLComponents = Record<string, IOperator>;
export interface DSL {
@@ -157,7 +156,7 @@ export interface IAgentForm {
delay_after_error: number;
visual_files_var: string;
max_rounds: number;
exception_method: Nullable<'comment' | 'go'>;
// exception_method: Nullable<'comment' | 'go'>;
exception_comment: any;
exception_goto: any;
tools: Array<{
@@ -275,5 +274,5 @@ export interface IPipeLineListRequest {
keywords?: string;
orderby?: string;
desc?: boolean;
canvas_category?: AgentCategory;
// canvas_category?: AgentCategory;
}

View File

@@ -1,4 +1,4 @@
import { MessageType } from '@/constants/chat';
// import { MessageType } from '@/constants/chat';
export interface PromptConfig {
empty_response: string;
@@ -89,7 +89,7 @@ export interface IConversation {
export interface Message {
content: string;
role: MessageType;
// role: MessageType;
doc_ids?: string[];
prompt?: string;
id?: string;

View File

@@ -1,4 +1,4 @@
import { Edge, Node } from '@xyflow/react';
import type { Edge, Node } from '@xyflow/react';
import type { IReference, Message } from './chat';
export type DSLComponents = Record<string, IOperator>;

View File

@@ -1,7 +1,7 @@
export enum McpServerType {
Sse = 'sse',
StreamableHttp = 'streamable-http',
}
// export enum McpServerType {
// Sse = 'sse',
// StreamableHttp = 'streamable-http',
// }
export interface IMcpServerVariable {
key: string;
@@ -12,7 +12,7 @@ export interface IMcpServerInfo {
id: string;
name: string;
url: string;
server_type: McpServerType;
// server_type: McpServerType;
description?: string;
variables?: IMcpServerVariable[];
headers: Map<string, string>;

View File

@@ -1,4 +1,4 @@
import { IExportedMcpServer } from '@/interfaces/database/mcp';
// import { IExportedMcpServer } from '@/interfaces/database/mcp';
export interface ITestMcpRequestBody {
server_type: string;
@@ -9,8 +9,8 @@ export interface ITestMcpRequestBody {
}
export interface IImportMcpServersRequestBody {
mcpServers: Record<
string,
Pick<IExportedMcpServer, 'type' | 'url' | 'authorization_token'>
>;
// mcpServers: Record<
// string,
// Pick<IExportedMcpServer, 'type' | 'url' | 'authorization_token'>
// >;
}

View File

@@ -61,7 +61,7 @@ const MetricValue = styled(Typography)(({ theme }) => ({
lineHeight: 1.2,
}));
const TrendIndicator = styled(Box)<{ trend: 'up' | 'down' }>(({ trend, theme }) => ({
const TrendIndicator = styled(Box)<{ trend: string }>(({ trend, theme }) => ({
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
@@ -245,7 +245,7 @@ const Dashboard: React.FC = () => {
{/* 关键指标卡片 */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<MetricCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -270,7 +270,7 @@ const Dashboard: React.FC = () => {
</MetricCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<MetricCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -295,7 +295,7 @@ const Dashboard: React.FC = () => {
</MetricCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<MetricCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -320,7 +320,7 @@ const Dashboard: React.FC = () => {
</MetricCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<MetricCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -362,7 +362,7 @@ const Dashboard: React.FC = () => {
</Button>
</Box>
<KnowledgeGridView
knowledgeBases={mockKnowledgeBases}
knowledgeBases={mockKnowledgeBases as any}
maxItems={3}
showSeeAll={true}
onSeeAll={handleSeeAllKnowledgeBases}
@@ -372,7 +372,7 @@ const Dashboard: React.FC = () => {
{/* 系统状态 */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} md={6}>
<Grid size={{xs:12,md:6}}>
<Card sx={{ border: '1px solid #E5E5E5' }}>
<CardContent>
<Typography variant="h6" fontWeight={600} mb={2}>
@@ -403,7 +403,7 @@ const Dashboard: React.FC = () => {
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Grid size={{xs:12,md:6}}>
<Card sx={{ border: '1px solid #E5E5E5' }}>
<CardContent>
<Typography variant="h6" fontWeight={600} mb={2}>
@@ -479,15 +479,6 @@ const Dashboard: React.FC = () => {
</TableContainer>
</CardContent>
</Card>
{/* 用户数据调试组件 - 仅在开发环境显示 */}
{process.env.NODE_ENV === 'development' && (
<Card sx={{ border: '1px solid #E5E5E5', mt: 3 }}>
<CardContent>
<UserDataDebug />
</CardContent>
</Card>
)}
</PageContainer>
);
};

View File

@@ -193,7 +193,7 @@ const MCP: React.FC = () => {
<>
{/* 状态概览 */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<StatusCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -211,7 +211,7 @@ const MCP: React.FC = () => {
</StatusCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<StatusCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -229,7 +229,7 @@ const MCP: React.FC = () => {
</StatusCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<StatusCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -247,7 +247,7 @@ const MCP: React.FC = () => {
</StatusCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<StatusCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -359,7 +359,7 @@ const MCP: React.FC = () => {
</Alert>
<Grid container spacing={2}>
{mockMCPServers.map((server) => (
<Grid item xs={12} md={6} key={server.id}>
<Grid size={{xs:12,md:6}} key={server.id}>
<Card variant="outlined">
<CardContent>
<Typography variant="subtitle1" fontWeight={600} mb={1}>
@@ -389,7 +389,7 @@ const MCP: React.FC = () => {
{tabValue === 2 && (
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Grid size={{xs:12,md:6}}>
<Card sx={{ border: '1px solid #E5E5E5' }}>
<CardContent>
<Typography variant="h6" fontWeight={600} mb={2}>
@@ -418,7 +418,7 @@ const MCP: React.FC = () => {
</Card>
</Grid>
<Grid item xs={12} md={6}>
<Grid size={{xs:12,md:6}}>
<Card sx={{ border: '1px solid #E5E5E5' }}>
<CardContent>
<Typography variant="h6" fontWeight={600} mb={2}>

View File

@@ -226,7 +226,7 @@ const ModelsResources: React.FC = () => {
<>
{/* 模型概览卡片 */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<ResourceCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -244,7 +244,7 @@ const ModelsResources: React.FC = () => {
</ResourceCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<ResourceCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -262,7 +262,7 @@ const ModelsResources: React.FC = () => {
</ResourceCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<ResourceCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -280,7 +280,7 @@ const ModelsResources: React.FC = () => {
</ResourceCard>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<ResourceCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -385,7 +385,7 @@ const ModelsResources: React.FC = () => {
<>
{/* 资源概览卡片 */}
<Grid container spacing={3} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6} md={4}>
<Grid size={{xs:12,sm:6,md:4}}>
<ResourceCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -403,7 +403,7 @@ const ModelsResources: React.FC = () => {
</ResourceCard>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Grid size={{xs:12,sm:6,md:4}}>
<ResourceCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">
@@ -421,7 +421,7 @@ const ModelsResources: React.FC = () => {
</ResourceCard>
</Grid>
<Grid item xs={12} sm={6} md={4}>
<Grid size={{xs:12,sm:6,md:4}}>
<ResourceCard>
<CardContent>
<Box display="flex" alignItems="center" justifyContent="space-between">

View File

@@ -155,7 +155,7 @@ const PipelineConfig: React.FC = () => {
</PageHeader>
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
<Grid size={{xs:12,md:6}}>
<ConfigCard>
<CardContent>
<Typography variant="h6" fontWeight={600} mb={2}>
@@ -223,7 +223,7 @@ const PipelineConfig: React.FC = () => {
</ConfigCard>
</Grid>
<Grid item xs={12} md={6}>
<Grid size={{xs:12,md:6}}>
<ConfigCard>
<CardContent>
<Typography variant="h6" fontWeight={600} mb={2}>
@@ -283,7 +283,7 @@ const PipelineConfig: React.FC = () => {
</ConfigCard>
</Grid>
<Grid item xs={12}>
<Grid size={{xs:12,md:6}}>
<ConfigCard>
<CardContent>
<Typography variant="h6" fontWeight={600} mb={2}>
@@ -291,7 +291,7 @@ const PipelineConfig: React.FC = () => {
</Typography>
<Grid container spacing={2}>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
<Typography variant="h4" color="primary" fontWeight={600}>
1,234
@@ -301,7 +301,7 @@ const PipelineConfig: React.FC = () => {
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
<Typography variant="h4" color="success.main" fontWeight={600}>
98.5%
@@ -311,7 +311,7 @@ const PipelineConfig: React.FC = () => {
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
<Typography variant="h4" color="warning.main" fontWeight={600}>
2.3s
@@ -321,7 +321,7 @@ const PipelineConfig: React.FC = () => {
</Typography>
</Box>
</Grid>
<Grid item xs={12} sm={6} md={3}>
<Grid size={{xs:12,sm:6,md:3}}>
<Box textAlign="center" p={2} bgcolor="#F8F9FA" borderRadius="6px">
<Typography variant="h4" color="info.main" fontWeight={600}>
156

View File

@@ -0,0 +1,351 @@
import React, { useMemo, useCallback } from 'react';
import {
ReactFlow,
type Node,
type Edge,
Controls,
Background,
useNodesState,
useEdgesState,
ConnectionMode,
Panel,
Handle,
Position,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { Box, Typography, Alert, Chip, Card, CardContent, Tooltip, Avatar } from '@mui/material';
import type { IKnowledgeGraph } from '@/interfaces/database/knowledge';
import { v4 as uuidv4 } from 'uuid';
interface KnowledgeGraphProps {
knowledgeGraph?: IKnowledgeGraph | null;
}
// 根据实体类型获取节点颜色
const getNodeColor = (entityType?: string): string => {
const colorMap: Record<string, string> = {
PERSON: '#FF6B6B', // 红色 - 人物
ORGANIZATION: '#4ECDC4', // 青色 - 组织
LOCATION: '#45B7D1', // 蓝色 - 地点
EVENT: '#96CEB4', // 绿色 - 事件
CONCEPT: '#FFEAA7', // 黄色 - 概念
CATEGORY: '#DDA0DD', // 紫色 - 分类
TECHNOLOGY: '#98D8C8', // 薄荷绿 - 技术
PRODUCT: '#F7DC6F', // 金黄色 - 产品
SERVICE: '#BB8FCE', // 淡紫色 - 服务
};
return colorMap[entityType || 'CONCEPT'] || '#74B9FF';
};
// 自定义节点组件
const CustomNode = ({ data }: { data: any }) => {
const nodeColor = getNodeColor(data.entity_type);
const nodeSize = Math.max(80, Math.min(140, (data.pagerank || 0.1) * 500));
// 获取节点首字母作为Avatar显示
const getInitials = (name: string) => {
if (!name) return '?';
const words = name.trim().split(/\s+/);
if (words.length === 1) {
return words[0].charAt(0).toUpperCase();
}
return words.slice(0, 2).map(word => word.charAt(0).toUpperCase()).join('');
};
const tooltipContent = (
<Box sx={{ p: 1, maxWidth: 300 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 1 }}>
{data.label || data.name || 'Unknown'}
</Typography>
<Typography variant="body2" sx={{ mb: 0.5 }}>
<strong>:</strong> {data.entity_type || 'Unknown'}
</Typography>
{data.description && (
<Typography variant="body2">
<strong>:</strong> {data.description}
</Typography>
)}
{data.pagerank && (
<Typography variant="caption" sx={{ display: 'block', mt: 1 }}>
PageRank: {data.pagerank.toFixed(4)}
</Typography>
)}
</Box>
);
return (
<Tooltip
title={tooltipContent}
placement="top"
arrow
enterDelay={500}
leaveDelay={200}
>
<Card
sx={{
width: nodeSize,
height: nodeSize,
borderRadius: '50%',
border: '3px solid #fff',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
cursor: 'pointer',
position: 'relative',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: nodeColor,
transition: 'all 0.2s ease-in-out',
'&:hover': {
transform: 'scale(1.1)',
boxShadow: '0 6px 20px rgba(0,0,0,0.25)',
zIndex: 10,
},
}}
>
{/* React Flow Handle 组件 */}
<Handle
type="target"
position={Position.Top}
style={{ background: '#555', opacity: 0 }}
/>
<Handle
type="source"
position={Position.Bottom}
style={{ background: '#555', opacity: 0 }}
/>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
p: 1,
'&:last-child': { pb: 1 },
height: '100%',
width: '100%',
}}
>
<Typography
variant="caption"
sx={{
color: '#fff',
fontWeight: 'bold',
fontSize: Math.max(10, nodeSize * 0.08),
textAlign: 'center',
lineHeight: 1.1,
wordBreak: 'break-word',
textShadow: '1px 1px 2px rgba(0,0,0,0.7)',
maxWidth: '90%',
overflow: 'hidden',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
}}
>
{data.label || data.name || 'Unknown'}
</Typography>
</CardContent>
</Card>
</Tooltip>
);
};
const nodeTypes = {
custom: CustomNode,
};
const KnowledgeGraphView: React.FC<KnowledgeGraphProps> = ({ knowledgeGraph }) => {
// 转换数据格式为 React Flow 所需的格式
const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
const graphData = knowledgeGraph?.graph || {};
if (!graphData?.nodes || !graphData?.edges) {
return { nodes: [], edges: [] };
}
// 转换节点数据
// @ts-ignore
const nodes: Node[] = graphData.nodes.map((node, index) => {
console.log(`节点 ${index}:`, node);
console.log(`节点ID: ${node.id}`);
const encodeId = encodeURIComponent(String(node.id));
const n: Node = {
id: encodeId,
type: 'custom',
position: {
x: Math.random() * 800,
y: Math.random() * 600,
},
data: {
label: node.entity_name || node.id,
entity_type: node.entity_type,
pagerank: node.pagerank,
description: node.description,
...node,
},
};
return n
});
// 转换边数据
// @ts-ignore
const edges: Edge[] = graphData.edges.map((edge, index) => {
console.log(`${index}:`, edge);
console.log(`src_id: ${edge.src_id}, tgt_id: ${edge.tgt_id}`);
// 检查source和target是否存在
if (!edge.src_id || !edge.tgt_id) {
console.warn(`${index} 缺少src_id或tgt_id:`, edge);
return null;
}
// 检查对应的节点是否存在
const sourceExists = nodes.some(node => node.id === encodeURIComponent(edge.src_id));
const targetExists = nodes.some(node => node.id === encodeURIComponent(edge.tgt_id));
if (!sourceExists || !targetExists) {
console.warn(`${index} 的节点不存在: source=${sourceExists}, target=${targetExists}`, edge);
return null;
}
const weight = Number(edge.weight) || 1;
const strokeWidth = Math.max(1, Math.min(3, weight));
const sourceId = encodeURIComponent(String(edge.src_id));
const targetId = encodeURIComponent(String(edge.tgt_id));
const edgeObj: Edge = {
id: `${sourceId}-${targetId}`,
source: sourceId,
target: targetId,
type: 'simplebezier',
// animated: weight > 5,
style: {
strokeWidth,
stroke: '#99ADD1',
},
// label: edge.description ? edge.description.substring(0, 50) + '...' : '',
labelStyle: {
fontSize: '10px',
fill: '#666',
},
data: {
weight: edge.weight,
description: edge.description,
keywords: edge.keywords,
},
};
return edgeObj;
})
// @ts-ignore
.filter(edge => edge != null); // 过滤掉null值
return { nodes, edges };
}, [knowledgeGraph]);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
console.log('转换后的节点:', nodes);
console.log('转换后的边:', edges);
// 节点点击事件
const onNodeClick = useCallback((event: React.MouseEvent, node: Node) => {
console.log('节点点击:', node.data);
}, []);
// 边点击事件
const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => {
console.log('边点击:', edge.data);
}, []);
if (!knowledgeGraph || initialNodes.length === 0) {
return (
<Box sx={{ p: 2, display: 'flex', justifyContent: 'center', alignItems: 'center', height: 400 }}>
<Alert severity="info"></Alert>
</Box>
);
}
return (
<Box sx={{ width: '100%', height: '600px', border: '1px solid #e0e0e0', borderRadius: 1 }}>
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
nodeTypes={nodeTypes}
connectionMode={ConnectionMode.Loose}
fitView
fitViewOptions={{ padding: 0.2 }}
minZoom={0.1}
maxZoom={2}
>
<Background />
<Controls />
{/* 图例面板 */}
<Panel position="top-right">
<Box sx={{
backgroundColor: 'rgba(255,255,255,0.9)',
p: 2,
borderRadius: 1,
minWidth: 200,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>
{['PERSON', 'ORGANIZATION', 'CATEGORY', 'TECHNOLOGY'].map((type) => (
<Box key={type} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Box
sx={{
width: 16,
height: 16,
borderRadius: '50%',
backgroundColor: getNodeColor(type),
border: '1px solid #fff',
}}
/>
<Typography variant="caption">{type}</Typography>
</Box>
))}
</Box>
</Box>
</Panel>
{/* 统计信息面板 */}
<Panel position="top-left">
<Box sx={{
backgroundColor: 'rgba(255,255,255,0.9)',
p: 2,
borderRadius: 1,
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}>
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 'bold' }}>
</Typography>
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip
label={`节点: ${nodes.length}`}
size="small"
color="primary"
variant="outlined"
/>
<Chip
label={`边: ${edges.length}`}
size="small"
color="secondary"
variant="outlined"
/>
</Box>
</Box>
</Panel>
</ReactFlow>
</Box>
);
};
export default KnowledgeGraphView;

View File

@@ -18,9 +18,10 @@ import {
CardContent,
Divider,
Chip,
Tabs,
Tab,
} from '@mui/material';
import { type GridRowSelectionModel } from '@mui/x-data-grid';
import knowledgeService from '@/services/knowledge_service';
import type { IKnowledge, IKnowledgeFile } from '@/interfaces/database/knowledge';
import type { IDocumentInfoFilter } from '@/interfaces/database/document';
import FileUploadDialog from '@/components/FileUploadDialog';
@@ -28,16 +29,16 @@ import KnowledgeInfoCard from './components/KnowledgeInfoCard';
import DocumentListComponent from './components/DocumentListComponent';
import FloatingActionButtons from './components/FloatingActionButtons';
import KnowledgeBreadcrumbs from './components/KnowledgeBreadcrumbs';
import KnowledgeGraphView from './components/KnowledgeGraphView';
import { useDocumentList, useDocumentOperations } from '@/hooks/document-hooks';
import { RUNNING_STATUS_KEYS } from '@/constants/knowledge';
import { useKnowledgeDetail } from '@/hooks/knowledge-hooks';
function KnowledgeBaseDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
// 状态管理
const [knowledgeBase, setKnowledgeBase] = useState<IKnowledge | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchKeyword, setSearchKeyword] = useState('');
const [rowSelectionModel, setRowSelectionModel] = useState<GridRowSelectionModel>({
@@ -46,11 +47,13 @@ function KnowledgeBaseDetail() {
});
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [testingDialogOpen, setTestingDialogOpen] = useState(false);
const [configDialogOpen, setConfigDialogOpen] = useState(false);
const [processDetailsDialogOpen, setProcessDetailsDialogOpen] = useState(false);
const [selectedFileDetails, setSelectedFileDetails] = useState<IKnowledgeFile | null>(null);
// 标签页状态
const [currentTab, setCurrentTab] = useState(0);
// 轮询相关状态
const pollingIntervalRef = useRef<any>(null);
const [isPolling, setIsPolling] = useState(false);
@@ -80,25 +83,9 @@ function KnowledgeBaseDetail() {
error: operationError,
} = useDocumentOperations();
// 获取知识库详情
const fetchKnowledgeDetail = async () => {
if (!id) return;
try {
setLoading(true);
const response = await knowledgeService.getKnowledgeDetail({ kb_id: id });
if (response.data.code === 0) {
setKnowledgeBase(response.data.data);
} else {
setError(response.data.message || '获取知识库详情失败');
}
} catch (err: any) {
setError(err.response?.data?.message || err.message || '获取知识库详情失败');
} finally {
setLoading(false);
}
};
const { knowledge: knowledgeBase, refresh: fetchKnowledgeDetail, loading, showKnowledgeGraph, knowledgeGraph } = useKnowledgeDetail(id || '');
console.log('showKnowledgeGraph:', showKnowledgeGraph, knowledgeGraph);
// 删除文件
const handleDeleteFiles = async () => {
@@ -291,42 +278,104 @@ function KnowledgeBaseDetail() {
{/* 知识库信息卡片 */}
<KnowledgeInfoCard knowledgeBase={knowledgeBase} />
{/* 文件列表组件 */}
<DocumentListComponent
files={files}
loading={filesLoading}
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
onReparse={(fileIds) => handleReparse(fileIds)}
onDelete={(fileIds) => {
console.log('删除文件:', fileIds);
setRowSelectionModel({
type: 'include',
ids: new Set(fileIds)
});
setDeleteDialogOpen(true);
}}
onUpload={() => setUploadDialogOpen(true)}
onRefresh={() => {
refreshFiles();
fetchKnowledgeDetail();
}}
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={(newModel) => {
console.log('新的选择模型:', newModel);
setRowSelectionModel(newModel);
}}
total={total}
page={currentPage}
pageSize={pageSize}
onPageChange={setCurrentPage}
onPageSizeChange={setPageSize}
onRename={handleRename}
onChangeStatus={handleChangeStatus}
onCancelRun={handleCancelRun}
onViewDetails={handleViewDetails}
onViewProcessDetails={handleViewProcessDetails}
/>
{/* 标签页组件 - 仅在showKnowledgeGraph为true时显示 */}
{showKnowledgeGraph ? (
<Box sx={{ mt: 3 }}>
<Tabs
value={currentTab}
onChange={(event, newValue) => setCurrentTab(newValue)}
sx={{ borderBottom: 1, borderColor: 'divider' }}
>
<Tab label="Documents" />
<Tab label="Graph" />
</Tabs>
{/* Document List 标签页内容 */}
{currentTab === 0 && (
<Box sx={{ mt: 2 }}>
<DocumentListComponent
files={files}
loading={filesLoading}
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
onReparse={(fileIds) => handleReparse(fileIds)}
onDelete={(fileIds) => {
console.log('删除文件:', fileIds);
setRowSelectionModel({
type: 'include',
ids: new Set(fileIds)
});
setDeleteDialogOpen(true);
}}
onUpload={() => setUploadDialogOpen(true)}
onRefresh={() => {
refreshFiles();
fetchKnowledgeDetail();
}}
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={(newModel) => {
console.log('新的选择模型:', newModel);
setRowSelectionModel(newModel);
}}
total={total}
page={currentPage}
pageSize={pageSize}
onPageChange={setCurrentPage}
onPageSizeChange={setPageSize}
onRename={handleRename}
onChangeStatus={handleChangeStatus}
onCancelRun={handleCancelRun}
onViewDetails={handleViewDetails}
onViewProcessDetails={handleViewProcessDetails}
/>
</Box>
)}
{/* Graph 标签页内容 */}
{currentTab === 1 && (
<Box sx={{ mt: 2 }}>
<KnowledgeGraphView knowledgeGraph={knowledgeGraph} />
</Box>
)}
</Box>
) : (
/* 原有的文件列表组件 - 当showKnowledgeGraph为false时显示 */
<DocumentListComponent
files={files}
loading={filesLoading}
searchKeyword={searchKeyword}
onSearchChange={setSearchKeyword}
onReparse={(fileIds) => handleReparse(fileIds)}
onDelete={(fileIds) => {
console.log('删除文件:', fileIds);
setRowSelectionModel({
type: 'include',
ids: new Set(fileIds)
});
setDeleteDialogOpen(true);
}}
onUpload={() => setUploadDialogOpen(true)}
onRefresh={() => {
refreshFiles();
fetchKnowledgeDetail();
}}
rowSelectionModel={rowSelectionModel}
onRowSelectionModelChange={(newModel) => {
console.log('新的选择模型:', newModel);
setRowSelectionModel(newModel);
}}
total={total}
page={currentPage}
pageSize={pageSize}
onPageChange={setCurrentPage}
onPageSizeChange={setPageSize}
onRename={handleRename}
onChangeStatus={handleChangeStatus}
onCancelRun={handleCancelRun}
onViewDetails={handleViewDetails}
onViewProcessDetails={handleViewProcessDetails}
/>
)}
{/* 浮动操作按钮 */}
<FloatingActionButtons
@@ -359,88 +408,6 @@ function KnowledgeBaseDetail() {
</DialogActions>
</Dialog>
{/* 检索测试对话框 */}
<Dialog open={testingDialogOpen} onClose={() => setTestingDialogOpen(false)} maxWidth="md" fullWidth>
<DialogTitle></DialogTitle>
<DialogContent>
<Stack spacing={3} sx={{ mt: 1 }}>
<TextField
fullWidth
label="测试查询"
placeholder="输入要测试的查询内容..."
multiline
rows={3}
/>
<Stack direction="row" spacing={2}>
<TextField
label="返回结果数量"
type="number"
defaultValue={5}
sx={{ width: 150 }}
/>
<TextField
label="相似度阈值"
type="number"
defaultValue={0.7}
inputProps={{ min: 0, max: 1, step: 0.1 }}
sx={{ width: 150 }}
/>
</Stack>
<Box sx={{ minHeight: 200, border: '1px solid #e0e0e0', borderRadius: 1, p: 2 }}>
<Typography variant="body2" color="text.secondary">
...
</Typography>
</Box>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setTestingDialogOpen(false)}></Button>
<Button variant="contained"></Button>
</DialogActions>
</Dialog>
{/* 配置设置对话框 */}
<Dialog open={configDialogOpen} onClose={() => setConfigDialogOpen(false)} maxWidth="sm" fullWidth>
<DialogTitle></DialogTitle>
<DialogContent>
<Stack spacing={3} sx={{ mt: 1 }}>
<TextField
fullWidth
label="知识库名称"
defaultValue={knowledgeBase?.name}
/>
<TextField
fullWidth
label="描述"
multiline
rows={3}
defaultValue={knowledgeBase?.description}
/>
<TextField
fullWidth
label="语言"
defaultValue={knowledgeBase?.language}
/>
<TextField
fullWidth
label="嵌入模型"
defaultValue={knowledgeBase?.embd_id}
disabled
/>
<TextField
fullWidth
label="解析器"
defaultValue={knowledgeBase?.parser_id}
disabled
/>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={() => setConfigDialogOpen(false)}></Button>
<Button variant="contained"></Button>
</DialogActions>
</Dialog>
{/* 文档详情对话框 */}
<Dialog
open={processDetailsDialogOpen}

View File

@@ -18,8 +18,8 @@
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,