diff --git a/package.json b/package.json index 988e678..0a606f7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 293b1cb..d399311 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/hooks/knowledge-hooks.ts b/src/hooks/knowledge-hooks.ts index 481d298..218ba71 100644 --- a/src/hooks/knowledge-hooks.ts +++ b/src/hooks/knowledge-hooks.ts @@ -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(null); + const [knowledgeGraph, setKnowledgeGraph] = useState(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, }; }; diff --git a/src/pages/knowledge/components/KnowledgeGraphView.tsx b/src/pages/knowledge/components/KnowledgeGraphView.tsx new file mode 100644 index 0000000..46ff0cf --- /dev/null +++ b/src/pages/knowledge/components/KnowledgeGraphView.tsx @@ -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 = { + 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 = ( + + + {data.label || data.name || 'Unknown'} + + + 类型: {data.entity_type || 'Unknown'} + + {data.description && ( + + 描述: {data.description} + + )} + {data.pagerank && ( + + PageRank: {data.pagerank.toFixed(4)} + + )} + + ); + + return ( + + + {/* React Flow Handle 组件 */} + + + + + + {data.label || data.name || 'Unknown'} + + + + + ); +}; + +const nodeTypes = { + custom: CustomNode, +}; + +const KnowledgeGraphView: React.FC = ({ 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 ( + + 暂无知识图谱数据 + + ); + } + + return ( + + + + + + {/* 图例面板 */} + + + + 图例 + + + {['PERSON', 'ORGANIZATION', 'CATEGORY', 'TECHNOLOGY'].map((type) => ( + + + {type} + + ))} + + + + + {/* 统计信息面板 */} + + + + 图谱统计 + + + + + + + + + + ); +}; + +export default KnowledgeGraphView; diff --git a/src/pages/knowledge/detail.tsx b/src/pages/knowledge/detail.tsx index f3506c6..ea44531 100644 --- a/src/pages/knowledge/detail.tsx +++ b/src/pages/knowledge/detail.tsx @@ -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(null); - const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [searchKeyword, setSearchKeyword] = useState(''); const [rowSelectionModel, setRowSelectionModel] = useState({ @@ -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(null); + // 标签页状态 + const [currentTab, setCurrentTab] = useState(0); + // 轮询相关状态 const pollingIntervalRef = useRef(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() { {/* 知识库信息卡片 */} - {/* 文件列表组件 */} - 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 ? ( + + setCurrentTab(newValue)} + sx={{ borderBottom: 1, borderColor: 'divider' }} + > + + + + + {/* Document List 标签页内容 */} + {currentTab === 0 && ( + + 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} + /> + + )} + + {/* Graph 标签页内容 */} + {currentTab === 1 && ( + + + + )} + + ) : ( + /* 原有的文件列表组件 - 当showKnowledgeGraph为false时显示 */ + 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} + /> + )} {/* 浮动操作按钮 */} - {/* 检索测试对话框 */} - setTestingDialogOpen(false)} maxWidth="md" fullWidth> - 检索测试 - - - - - - - - - - 测试结果将在这里显示... - - - - - - - - - - - {/* 配置设置对话框 */} - setConfigDialogOpen(false)} maxWidth="sm" fullWidth> - 配置设置 - - - - - - - - - - - - - - - {/* 文档详情对话框 */}