From ad6f0a1ce5ad0d5e122c84a6fb7d52c3c27bd1f3 Mon Sep 17 00:00:00 2001 From: balibabu Date: Tue, 16 Apr 2024 19:06:47 +0800 Subject: [PATCH] feat: add overview (#391) ### What problem does this PR solve? feat: render stats charts feat: create api token feat: delete api token feat: add ChatApiKeyModal feat: add RagLineChart Issue link: #345 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/package-lock.json | 345 +++++++++++++++++- web/package.json | 5 +- web/src/app.tsx | 15 + web/src/components/copy-to-clipboard.tsx | 27 ++ web/src/components/line-chart/index.tsx | 88 +++++ web/src/hooks/chatHooks.ts | 86 ++++- web/src/interfaces/database/chat.ts | 18 + web/src/locales/en.ts | 20 + web/src/locales/zh-traditional.ts | 26 +- web/src/locales/zh.ts | 24 +- .../components/knowledge-file/index.tsx | 4 + .../pages/chat/chat-api-key-modal/index.tsx | 70 ++++ .../assistant-setting.tsx | 11 +- .../prompt-engine.tsx | 10 +- .../pages/chat/chat-overview-modal/index.less | 21 ++ .../pages/chat/chat-overview-modal/index.tsx | 97 +++++ web/src/pages/chat/hooks.ts | 113 +++++- web/src/pages/chat/index.tsx | 42 ++- web/src/pages/chat/model.ts | 97 ++++- web/src/services/chatService.ts | 35 ++ web/src/utils/api.ts | 20 +- web/src/utils/commonUtil.ts | 17 + web/src/utils/date.ts | 10 +- web/src/utils/registerServer.ts | 10 +- web/src/utils/request.ts | 6 + 25 files changed, 1177 insertions(+), 40 deletions(-) create mode 100644 web/src/components/copy-to-clipboard.tsx create mode 100644 web/src/components/line-chart/index.tsx create mode 100644 web/src/pages/chat/chat-api-key-modal/index.tsx create mode 100644 web/src/pages/chat/chat-overview-modal/index.less create mode 100644 web/src/pages/chat/chat-overview-modal/index.tsx create mode 100644 web/src/utils/commonUtil.ts diff --git a/web/package-lock.json b/web/package-lock.json index 318cc80d70..82f2284910 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,19 +13,21 @@ "antd": "^5.12.7", "axios": "^1.6.3", "classnames": "^2.5.1", + "dayjs": "^1.11.10", "i18next": "^23.7.16", "js-base64": "^3.7.5", "jsencrypt": "^3.3.2", "lodash": "^4.17.21", - "moment": "^2.30.1", "rc-tween-one": "^3.0.6", "react-chat-elements": "^12.0.13", + "react-copy-to-clipboard": "^5.1.0", "react-i18next": "^14.0.0", "react-infinite-scroll-component": "^6.1.0", "react-markdown": "^9.0.1", "react-pdf-highlighter": "^6.1.0", "react-string-replace": "^1.1.1", "react-syntax-highlighter": "^15.5.0", + "recharts": "^2.12.4", "remark-gfm": "^4.0.0", "umi": "^4.0.90", "umi-request": "^1.4.0", @@ -36,6 +38,7 @@ "@react-dev-inspector/umi4-plugin": "^2.0.1", "@types/lodash": "^4.14.202", "@types/react": "^18.0.33", + "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18.0.11", "@types/react-syntax-highlighter": "^15.5.11", "@types/uuid": "^9.0.8", @@ -2676,6 +2679,60 @@ "@babel/types": "^7.20.7" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.8.tgz", + "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.6", + "resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.6.tgz", + "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.3.tgz", + "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", @@ -2884,6 +2941,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-copy-to-clipboard": { + "version": "5.0.7", + "resolved": "https://registry.npmmirror.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.7.tgz", + "integrity": "sha512-Gft19D+as4M+9Whq1oglhmK49vqPhcLzk8WfvfLvaYMIPYanyfLy0+CwFucMJfdKoSFyySPmkkWn8/E6voQXjQ==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-dom": { "version": "18.2.18", "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.2.18.tgz", @@ -5832,6 +5898,14 @@ "node": ">=12" } }, + "node_modules/clsx": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.0.tgz", + "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "engines": { + "node": ">=6" + } + }, "node_modules/coa": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/coa/-/coa-2.0.2.tgz", @@ -6640,11 +6714,132 @@ "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-1.2.4.tgz", "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-polygon": { "version": "1.0.6", "resolved": "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-1.0.6.tgz", "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ==" }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", @@ -6705,6 +6900,11 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmmirror.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -7032,6 +7232,15 @@ "utila": "~0.4" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, "node_modules/dom-serializer": { "version": "1.4.1", "resolved": "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-1.4.1.tgz", @@ -8151,6 +8360,11 @@ "es5-ext": "~0.10.14" } }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmmirror.com/events/-/events-3.3.0.tgz", @@ -8356,6 +8570,14 @@ "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, + "node_modules/fast-equals": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/fast-equals/-/fast-equals-5.0.1.tgz", + "integrity": "sha512-WF1Wi8PwwSY7/6Kx0vKXtw8RwuSGoM1bvDaJbu7MxDlR1vovZjIAKrnzyrThgAjm6JDTu0fVgWXDlMGspodfoQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/fast-glob": { "version": "3.2.12", "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.2.12.tgz", @@ -9693,6 +9915,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/intersection-observer": { "version": "0.12.2", "resolved": "https://registry.npmmirror.com/intersection-observer/-/intersection-observer-0.12.2.tgz", @@ -11925,6 +12155,7 @@ "version": "2.30.1", "resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz", "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "devOptional": true, "engines": { "node": "*" } @@ -14356,6 +14587,18 @@ "react-dom": "18.2.0" } }, + "node_modules/react-copy-to-clipboard": { + "version": "5.1.0", + "resolved": "https://registry.npmmirror.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", + "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", + "dependencies": { + "copy-to-clipboard": "^3.3.1", + "prop-types": "^15.8.1" + }, + "peerDependencies": { + "react": "^15.3.0 || 16 || 17 || 18" + } + }, "node_modules/react-dev-inspector": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/react-dev-inspector/-/react-dev-inspector-2.0.1.tgz", @@ -14934,6 +15177,20 @@ "react": ">=15" } }, + "node_modules/react-smooth": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/react-smooth/-/react-smooth-4.0.1.tgz", + "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-spinkit": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/react-spinkit/-/react-spinkit-3.0.0.tgz", @@ -14968,6 +15225,21 @@ "react": ">= 0.14.0" } }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmmirror.com/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, "node_modules/reactcss": { "version": "1.2.3", "resolved": "https://registry.npmmirror.com/reactcss/-/reactcss-1.2.3.tgz", @@ -15145,6 +15417,41 @@ "node": ">= 12.13.0" } }, + "node_modules/recharts": { + "version": "2.12.4", + "resolved": "https://registry.npmmirror.com/recharts/-/recharts-2.12.4.tgz", + "integrity": "sha512-dM4skmk4fDKEDjL9MNunxv6zcTxePGVEzRnLDXALRpfJ85JoQ0P0APJ/CoJlmnQI0gPjBlOkjzrwrfQrRST3KA==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^16.10.2", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmmirror.com/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmmirror.com/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -17000,9 +17307,7 @@ "node_modules/tiny-invariant": { "version": "1.3.1", "resolved": "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz", - "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==", - "dev": true, - "peer": true + "integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==" }, "node_modules/tiny-warning": { "version": "1.0.3", @@ -18221,6 +18526,38 @@ "unist-util-stringify-position": "^4.0.0" } }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/victory-vendor/node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/vite": { "version": "4.3.1", "resolved": "https://registry.npmmirror.com/vite/-/vite-4.3.1.tgz", diff --git a/web/package.json b/web/package.json index 48a60b8d21..861d96b17d 100644 --- a/web/package.json +++ b/web/package.json @@ -17,19 +17,21 @@ "antd": "^5.12.7", "axios": "^1.6.3", "classnames": "^2.5.1", + "dayjs": "^1.11.10", "i18next": "^23.7.16", "js-base64": "^3.7.5", "jsencrypt": "^3.3.2", "lodash": "^4.17.21", - "moment": "^2.30.1", "rc-tween-one": "^3.0.6", "react-chat-elements": "^12.0.13", + "react-copy-to-clipboard": "^5.1.0", "react-i18next": "^14.0.0", "react-infinite-scroll-component": "^6.1.0", "react-markdown": "^9.0.1", "react-pdf-highlighter": "^6.1.0", "react-string-replace": "^1.1.1", "react-syntax-highlighter": "^15.5.0", + "recharts": "^2.12.4", "remark-gfm": "^4.0.0", "umi": "^4.0.90", "umi-request": "^1.4.0", @@ -40,6 +42,7 @@ "@react-dev-inspector/umi4-plugin": "^2.0.1", "@types/lodash": "^4.14.202", "@types/react": "^18.0.33", + "@types/react-copy-to-clipboard": "^5.0.7", "@types/react-dom": "^18.0.11", "@types/react-syntax-highlighter": "^15.5.11", "@types/uuid": "^9.0.8", diff --git a/web/src/app.tsx b/web/src/app.tsx index e80c9b922e..ff532b1d35 100644 --- a/web/src/app.tsx +++ b/web/src/app.tsx @@ -6,6 +6,21 @@ import zh_HK from 'antd/locale/zh_HK'; import React, { ReactNode, useEffect, useState } from 'react'; import storage from './utils/authorizationUtil'; +import dayjs from 'dayjs'; +import advancedFormat from 'dayjs/plugin/advancedFormat'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; +import localeData from 'dayjs/plugin/localeData'; +import weekday from 'dayjs/plugin/weekday'; +import weekOfYear from 'dayjs/plugin/weekOfYear'; +import weekYear from 'dayjs/plugin/weekYear'; + +dayjs.extend(customParseFormat); +dayjs.extend(advancedFormat); +dayjs.extend(weekday); +dayjs.extend(localeData); +dayjs.extend(weekOfYear); +dayjs.extend(weekYear); + const AntLanguageMap = { en: enUS, zh: zhCN, diff --git a/web/src/components/copy-to-clipboard.tsx b/web/src/components/copy-to-clipboard.tsx new file mode 100644 index 0000000000..ed2af3a2a6 --- /dev/null +++ b/web/src/components/copy-to-clipboard.tsx @@ -0,0 +1,27 @@ +import { useTranslate } from '@/hooks/commonHooks'; +import { CheckOutlined, CopyOutlined } from '@ant-design/icons'; +import { Tooltip } from 'antd'; +import { useState } from 'react'; +import { CopyToClipboard as Clipboard, Props } from 'react-copy-to-clipboard'; + +const CopyToClipboard = ({ text }: Props) => { + const [copied, setCopied] = useState(false); + const { t } = useTranslate('common'); + + const handleCopy = () => { + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 2000); + }; + + return ( + + + {copied ? : } + + + ); +}; + +export default CopyToClipboard; diff --git a/web/src/components/line-chart/index.tsx b/web/src/components/line-chart/index.tsx new file mode 100644 index 0000000000..6c032206f0 --- /dev/null +++ b/web/src/components/line-chart/index.tsx @@ -0,0 +1,88 @@ +import { + CartesianGrid, + Legend, + Line, + LineChart, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { CategoricalChartProps } from 'recharts/types/chart/generateCategoricalChart'; + +const data = [ + { + name: 'Page A', + uv: 4000, + pv: 2400, + }, + { + name: 'Page B', + uv: 3000, + pv: 1398, + }, + { + name: 'Page C', + uv: 2000, + pv: 9800, + }, + { + name: 'Page D', + uv: 2780, + pv: 3908, + }, + { + name: 'Page E', + uv: 1890, + pv: 4800, + }, + { + name: 'Page F', + uv: 2390, + pv: 3800, + }, + { + name: 'Page G', + uv: 3490, + pv: 4300, + }, +]; + +interface IProps extends CategoricalChartProps { + data?: Array<{ xAxis: string; yAxis: number }>; +} + +const RagLineChart = ({ data }: IProps) => { + return ( + + + + + + + + + {/* */} + + + ); +}; + +export default RagLineChart; diff --git a/web/src/hooks/chatHooks.ts b/web/src/hooks/chatHooks.ts index 7bf9a2b9f3..90d419d214 100644 --- a/web/src/hooks/chatHooks.ts +++ b/web/src/hooks/chatHooks.ts @@ -1,4 +1,9 @@ -import { IConversation, IDialog } from '@/interfaces/database/chat'; +import { + IConversation, + IDialog, + IStats, + IToken, +} from '@/interfaces/database/chat'; import { useCallback } from 'react'; import { useDispatch, useSelector } from 'umi'; @@ -164,3 +169,82 @@ export const useCompleteConversation = () => { return completeConversation; }; + +// #region API provided for external calls + +export const useCreateToken = (dialogId: string) => { + const dispatch = useDispatch(); + + const createToken = useCallback(() => { + return dispatch({ + type: 'chatModel/createToken', + payload: { dialogId }, + }); + }, [dispatch, dialogId]); + + return createToken; +}; + +export const useListToken = () => { + const dispatch = useDispatch(); + + const listToken = useCallback( + (dialogId: string) => { + return dispatch({ + type: 'chatModel/listToken', + payload: { dialogId }, + }); + }, + [dispatch], + ); + + return listToken; +}; + +export const useSelectTokenList = () => { + const tokenList: IToken[] = useSelector( + (state: any) => state.chatModel.tokenList, + ); + + return tokenList; +}; + +export const useRemoveToken = () => { + const dispatch = useDispatch(); + + const removeToken = useCallback( + (payload: { tenantId: string; dialogId: string; tokens: string[] }) => { + return dispatch({ + type: 'chatModel/removeToken', + payload: payload, + }); + }, + [dispatch], + ); + + return removeToken; +}; + +export const useFetchStats = () => { + const dispatch = useDispatch(); + + const fetchStats = useCallback( + (payload: any) => { + return dispatch({ + type: 'chatModel/getStats', + payload, + }); + }, + [dispatch], + ); + + return fetchStats; +}; + +export const useSelectStats = () => { + const stats: IStats = useSelector((state: any) => state.chatModel.stats); + + return stats; +}; + +//#endregion diff --git a/web/src/interfaces/database/chat.ts b/web/src/interfaces/database/chat.ts index 76b590912a..a2c53032e8 100644 --- a/web/src/interfaces/database/chat.ts +++ b/web/src/interfaces/database/chat.ts @@ -91,3 +91,21 @@ export interface Docagg { // term_similarity: number; // vector_similarity: number; // } + +export interface IToken { + create_date: string; + create_time: number; + tenant_id: string; + token: string; + update_date?: any; + update_time?: any; +} + +export interface IStats { + pv: [string, number][]; + uv: [string, number][]; + speed: [string, number][]; + tokens: [string, number][]; + round: [string, number][]; + thumb_up: [string, number][]; +} diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 430250bde2..07c61f423f 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -20,6 +20,8 @@ export default { language: 'Language', languageMessage: 'Please input your language!', languagePlaceholder: 'select your language', + copy: 'Copy', + copied: 'Copied', }, login: { login: 'Sign in', @@ -335,6 +337,24 @@ export default { 'This sets the maximum length of the model’s output, measured in the number of tokens (words or pieces of words).', quote: 'Show Quote', quoteTip: 'Should the source of the original text be displayed?', + overview: 'Overview', + pv: 'Number of messages', + uv: 'Active user number', + speed: 'Token output speed', + tokens: 'Consume the token number', + round: 'Session Interaction Number', + thumbUp: 'customer satisfaction', + publicUrl: 'Public URL', + preview: 'Preview', + embedded: 'Embedded', + serviceApiEndpoint: 'Service API Endpoint', + apiKey: 'Api Key', + apiReference: 'Api Reference', + dateRange: 'Date Range:', + backendServiceApi: 'Backend service API', + createNewKey: 'Create new key', + created: 'Created', + action: 'Action', }, setting: { profile: 'Profile', diff --git a/web/src/locales/zh-traditional.ts b/web/src/locales/zh-traditional.ts index b6af198966..6ea310d815 100644 --- a/web/src/locales/zh-traditional.ts +++ b/web/src/locales/zh-traditional.ts @@ -15,11 +15,13 @@ export default { edit: '編輯', upload: '上傳', english: '英語', - chinese: '中文簡體', - traditionalChinese: '中文繁體', + chinese: '簡體中文', + traditionalChinese: '繁體中文', language: '語言', languageMessage: '請輸入語言', languagePlaceholder: '請選擇語言', + copy: '複製', + copied: '複製成功', }, login: { login: '登入', @@ -269,7 +271,7 @@ export default { systemMessage: '請輸入', systemTip: '當LLM回答問題時,你需要LLM遵循的說明,比如角色設計、答案長度和答案語言等。', - topN: 'top n', + topN: 'Top N', topNTip: `並非所有相似度得分高於“相似度閾值”的塊都會被提供給法學碩士。LLM 只能看到這些“Top N”塊。`, variable: '變量', variableTip: `如果您使用对话 API,变量可能会帮助您使用不同的策略与客户聊天。 @@ -310,6 +312,24 @@ export default { '這設置了模型輸出的最大長度,以標記(單詞或單詞片段)的數量來衡量。', quote: '顯示引文', quoteTip: '是否應該顯示原文出處?', + overview: '概覽', + pv: '消息數', + uv: '活躍用戶數', + speed: 'Token 輸出速度', + tokens: '消耗Token數', + round: '會話互動數', + thumbUp: '用戶滿意度', + publicUrl: '公共url', + preview: '預覽', + embedded: '嵌入', + serviceApiEndpoint: '服務API端點', + apiKey: 'API鍵', + apiReference: 'API參考', + dateRange: '日期範圍:', + backendServiceApi: '後端服務API', + createNewKey: '創建新密鑰', + created: '創建於', + action: '操作', }, setting: { profile: '概述', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index e2bbd04828..bf97cbf41a 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -15,11 +15,13 @@ export default { edit: '编辑', upload: '上传', english: '英文', - chinese: '中文简体', - traditionalChinese: '中文繁体', + chinese: '简体中文', + traditionalChinese: '繁体中文', language: '语言', languageMessage: '请输入语言', languagePlaceholder: '请选择语言', + copy: '复制', + copied: '复制成功', }, login: { login: '登录', @@ -326,6 +328,24 @@ export default { '这设置了模型输出的最大长度,以标记(单词或单词片段)的数量来衡量。', quote: '显示引文', quoteTip: '是否应该显示原文出处?', + overview: '概览', + pv: '消息数', + uv: '活跃用户数', + speed: 'Token 输出速度', + tokens: '消耗Token数', + round: '会话互动数', + thumbUp: '用户满意度', + publicUrl: '公共Url', + preview: '预览', + embedded: '嵌入', + serviceApiEndpoint: '服务API端点', + apiKey: 'API键', + apiReference: 'API参考', + dateRange: '日期范围:', + backendServiceApi: '后端服务API', + createNewKey: '创建新密钥', + created: '创建于', + action: '操作', }, setting: { profile: '概要', diff --git a/web/src/pages/add-knowledge/components/knowledge-file/index.tsx b/web/src/pages/add-knowledge/components/knowledge-file/index.tsx index 765d67bc86..a6bd7584d5 100644 --- a/web/src/pages/add-knowledge/components/knowledge-file/index.tsx +++ b/web/src/pages/add-knowledge/components/knowledge-file/index.tsx @@ -26,6 +26,7 @@ import ParsingActionCell from './parsing-action-cell'; import ParsingStatusCell from './parsing-status-cell'; import RenameModal from './rename-modal'; +import { formatDate } from '@/utils/date'; import styles from './index.less'; const KnowledgeFile = () => { @@ -94,6 +95,9 @@ const KnowledgeFile = () => { title: t('uploadDate'), dataIndex: 'create_date', key: 'create_date', + render(value) { + return formatDate(value); + }, }, { title: t('chunkMethod'), diff --git a/web/src/pages/chat/chat-api-key-modal/index.tsx b/web/src/pages/chat/chat-api-key-modal/index.tsx new file mode 100644 index 0000000000..b4545a40aa --- /dev/null +++ b/web/src/pages/chat/chat-api-key-modal/index.tsx @@ -0,0 +1,70 @@ +import CopyToClipboard from '@/components/copy-to-clipboard'; +import { useTranslate } from '@/hooks/commonHooks'; +import { IModalProps } from '@/interfaces/common'; +import { IToken } from '@/interfaces/database/chat'; +import { formatDate } from '@/utils/date'; +import { DeleteOutlined } from '@ant-design/icons'; +import type { TableProps } from 'antd'; +import { Button, Modal, Space, Table } from 'antd'; +import { useOperateApiKey } from '../hooks'; + +const ChatApiKeyModal = ({ + visible, + dialogId, + hideModal, +}: IModalProps & { dialogId: string }) => { + const { createToken, removeToken, tokenList, listLoading, creatingLoading } = + useOperateApiKey(visible, dialogId); + const { t } = useTranslate('chat'); + + const columns: TableProps['columns'] = [ + { + title: 'Token', + dataIndex: 'token', + key: 'token', + render: (text) => {text}, + }, + { + title: t('created'), + dataIndex: 'create_date', + key: 'create_date', + render: (text) => formatDate(text), + }, + { + title: t('action'), + key: 'action', + render: (_, record) => ( + + + removeToken(record.token, record.tenant_id)} + /> + + ), + }, + ]; + + return ( + <> + + + + + + ); +}; + +export default ChatApiKeyModal; diff --git a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx index 0f4ba92010..fdaa08c299 100644 --- a/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx +++ b/web/src/pages/chat/chat-configuration-modal/assistant-setting.tsx @@ -1,6 +1,6 @@ import { useFetchKnowledgeList } from '@/hooks/knowledgeHook'; import { PlusOutlined } from '@ant-design/icons'; -import { Form, Input, Select, Upload } from 'antd'; +import { Form, Input, Select, Switch, Upload } from 'antd'; import classNames from 'classnames'; import { ISegmentedContentProps } from '../interface'; @@ -83,6 +83,15 @@ const AssistantSetting = ({ show }: ISegmentedContentProps) => { > + + + - - - +
diff --git a/web/src/pages/chat/chat-overview-modal/index.less b/web/src/pages/chat/chat-overview-modal/index.less new file mode 100644 index 0000000000..1a376715df --- /dev/null +++ b/web/src/pages/chat/chat-overview-modal/index.less @@ -0,0 +1,21 @@ +.chartWrapper { + height: 40vh; + overflow: auto; +} + +.chartItem { + height: 300px; + padding: 10px 0 30px; +} + +.chartLabel { + display: inline-block; + padding-left: 60px; + padding-bottom: 20px; +} +.linkText { + border-radius: 6px; + padding: 6px 10px; + background-color: #eff8ff; + border: 1px; +} diff --git a/web/src/pages/chat/chat-overview-modal/index.tsx b/web/src/pages/chat/chat-overview-modal/index.tsx new file mode 100644 index 0000000000..9d3336dc54 --- /dev/null +++ b/web/src/pages/chat/chat-overview-modal/index.tsx @@ -0,0 +1,97 @@ +import LineChart from '@/components/line-chart'; +import { useSetModalState, useTranslate } from '@/hooks/commonHooks'; +import { IModalProps } from '@/interfaces/common'; +import { IDialog, IStats } from '@/interfaces/database/chat'; +import { Button, Card, DatePicker, Flex, Modal, Space, Typography } from 'antd'; +import { RangePickerProps } from 'antd/es/date-picker'; +import dayjs from 'dayjs'; +import camelCase from 'lodash/camelCase'; +import ChatApiKeyModal from '../chat-api-key-modal'; +import { useFetchStatsOnMount, useSelectChartStatsList } from '../hooks'; +import styles from './index.less'; + +const { Paragraph } = Typography; +const { RangePicker } = DatePicker; + +const ChatOverviewModal = ({ + visible, + hideModal, + dialog, +}: IModalProps & { dialog: IDialog }) => { + const { t } = useTranslate('chat'); + const chartList = useSelectChartStatsList(); + + const { + visible: apiKeyVisible, + hideModal: hideApiKeyModal, + showModal: showApiKeyModal, + } = useSetModalState(); + + const { pickerValue, setPickerValue } = useFetchStatsOnMount(visible); + + const disabledDate: RangePickerProps['disabledDate'] = (current) => { + return current && current > dayjs().endOf('day'); + }; + + return ( + <> + + + + + {t('publicUrl')} + + This is a copyable text. + + + + + + + + + + {t('serviceApiEndpoint')} + + This is a copyable text. + + + + + + + + + {t('dateRange')} + + +
+ {Object.keys(chartList).map((x) => ( +
+ {t(camelCase(x))} + +
+ ))} +
+
+ +
+ + ); +}; + +export default ChatOverviewModal; diff --git a/web/src/pages/chat/hooks.ts b/web/src/pages/chat/hooks.ts index b4cf9ca207..10100b48ce 100644 --- a/web/src/pages/chat/hooks.ts +++ b/web/src/pages/chat/hooks.ts @@ -2,22 +2,28 @@ import { MessageType } from '@/constants/chat'; import { fileIconMap } from '@/constants/common'; import { useCompleteConversation, + useCreateToken, useFetchConversation, useFetchConversationList, useFetchDialog, useFetchDialogList, + useFetchStats, + useListToken, useRemoveConversation, useRemoveDialog, + useRemoveToken, useSelectConversationList, useSelectDialogList, + useSelectTokenList, useSetDialog, useUpdateConversation, } from '@/hooks/chatHooks'; import { useSetModalState, useShowDeleteConfirm } from '@/hooks/commonHooks'; import { useOneNamespaceEffectsLoading } from '@/hooks/storeHooks'; -import { IConversation, IDialog } from '@/interfaces/database/chat'; +import { IConversation, IDialog, IStats } from '@/interfaces/database/chat'; import { IChunk } from '@/interfaces/database/knowledge'; import { getFileExtension } from '@/utils'; +import dayjs, { Dayjs } from 'dayjs'; import omit from 'lodash/omit'; import { ChangeEventHandler, @@ -704,3 +710,108 @@ export const useGetSendButtonDisabled = () => { return dialogId === '' && conversationId === ''; }; //#endregion + +//#region API provided for external calls + +type RangeValue = [Dayjs | null, Dayjs | null] | null; + +export const useFetchStatsOnMount = (visible: boolean) => { + const fetchStats = useFetchStats(); + const [pickerValue, setPickerValue] = useState([ + dayjs(), + dayjs().subtract(7, 'day'), + ]); + + useEffect(() => { + if (visible && Array.isArray(pickerValue) && pickerValue[0]) { + fetchStats({ fromDate: pickerValue[0], toDate: pickerValue[1] }); + } + }, [fetchStats, pickerValue, visible]); + + return { + pickerValue, + setPickerValue, + }; +}; + +export const useOperateApiKey = (visible: boolean, dialogId: string) => { + const removeToken = useRemoveToken(); + const createToken = useCreateToken(dialogId); + const listToken = useListToken(); + const tokenList = useSelectTokenList(); + const creatingLoading = useOneNamespaceEffectsLoading('chatModel', [ + 'createToken', + ]); + const listLoading = useOneNamespaceEffectsLoading('chatModel', ['list']); + + const showDeleteConfirm = useShowDeleteConfirm(); + + const onRemoveToken = (token: string, tenantId: string) => { + showDeleteConfirm({ + onOk: () => removeToken({ dialogId, tokens: [token], tenantId }), + }); + }; + + useEffect(() => { + if (visible && dialogId) { + listToken(dialogId); + } + }, [listToken, dialogId, visible]); + + return { + removeToken: onRemoveToken, + createToken, + tokenList, + creatingLoading, + listLoading, + }; +}; + +type ChartStatsType = { + [k in keyof IStats]: Array<{ xAxis: string; yAxis: number }>; +}; + +export const useSelectChartStatsList = (): ChartStatsType => { + // const stats: IStats = useSelectStats(); + const stats = { + pv: [ + ['2024-06-01', 1], + ['2024-07-24', 3], + ['2024-09-01', 10], + ], + uv: [ + ['2024-02-01', 0], + ['2024-03-01', 99], + ['2024-05-01', 3], + ], + speed: [ + ['2024-09-01', 2], + ['2024-09-01', 3], + ], + tokens: [ + ['2024-09-01', 1], + ['2024-09-01', 3], + ], + round: [ + ['2024-09-01', 0], + ['2024-09-01', 3], + ], + thumb_up: [ + ['2024-09-01', 3], + ['2024-09-01', 9], + ], + }; + + return Object.keys(stats).reduce((pre, cur) => { + const item = stats[cur as keyof IStats]; + if (item.length > 0) { + pre[cur as keyof IStats] = item.map((x) => ({ + xAxis: x[0] as string, + yAxis: x[1] as number, + })); + } + return pre; + }, {} as ChartStatsType); +}; + +//#endregion diff --git a/web/src/pages/chat/index.tsx b/web/src/pages/chat/index.tsx index 38f826c9a4..b07897bc18 100644 --- a/web/src/pages/chat/index.tsx +++ b/web/src/pages/chat/index.tsx @@ -35,7 +35,10 @@ import { useSelectFirstDialogOnMount, } from './hooks'; -import { useTranslate } from '@/hooks/commonHooks'; +import { useSetModalState, useTranslate } from '@/hooks/commonHooks'; +import { useSetSelectedRecord } from '@/hooks/logicHooks'; +import { IDialog } from '@/interfaces/database/chat'; +import ChatOverviewModal from './chat-overview-modal'; import styles from './index.less'; const Chat = () => { @@ -73,6 +76,12 @@ const Chat = () => { const dialogLoading = useSelectDialogListLoading(); const conversationLoading = useSelectConversationListLoading(); const { t } = useTranslate('chat'); + const { + visible: overviewVisible, + hideModal: hideOverviewModal, + showModal: showOverviewModal, + } = useSetModalState(); + const { currentRecord, setRecord } = useSetSelectedRecord(); useFetchDialogOnMount(dialogId, true); @@ -100,6 +109,15 @@ const Chat = () => { onRemoveDialog([dialogId]); }; + const handleShowOverviewModal = + (dialog: IDialog): any => + (info: any) => { + info?.domEvent?.preventDefault(); + info?.domEvent?.stopPropagation(); + setRecord(dialog); + showOverviewModal(); + }; + const handleRemoveConversation = (conversationId: string): MenuItemProps['onClick'] => ({ domEvent }) => { @@ -141,7 +159,9 @@ const Chat = () => { }, ]; - const buildAppItems = (dialogId: string) => { + const buildAppItems = (dialog: IDialog) => { + const dialogId = dialog.id; + const appItems: MenuProps['items'] = [ { key: '1', @@ -164,6 +184,17 @@ const Chat = () => { ), }, + { type: 'divider' }, + // { + // key: '3', + // onClick: handleShowOverviewModal(dialog), + // label: ( + // + // + // {t('overview')} + // + // ), + // }, ]; return appItems; @@ -230,7 +261,7 @@ const Chat = () => { {activated === x.id && (
- + @@ -315,6 +346,11 @@ const Chat = () => { initialName={initialConversationName} loading={conversationRenameLoading} > + ); }; diff --git a/web/src/pages/chat/model.ts b/web/src/pages/chat/model.ts index 51eb71110e..c2498a012d 100644 --- a/web/src/pages/chat/model.ts +++ b/web/src/pages/chat/model.ts @@ -1,7 +1,14 @@ -import { IConversation, IDialog, Message } from '@/interfaces/database/chat'; +import { + IConversation, + IDialog, + IStats, + IToken, + Message, +} from '@/interfaces/database/chat'; import i18n from '@/locales/config'; import chatService from '@/services/chatService'; import { message } from 'antd'; +import omit from 'lodash/omit'; import { DvaModel } from 'umi'; import { v4 as uuid } from 'uuid'; import { IClientConversation, IMessage } from './interface'; @@ -13,6 +20,8 @@ export interface ChatModelState { currentDialog: IDialog; conversationList: IConversation[]; currentConversation: IClientConversation; + tokenList: IToken[]; + stats: IStats; } const model: DvaModel = { @@ -23,6 +32,8 @@ const model: DvaModel = { currentDialog: {}, conversationList: [], currentConversation: {} as IClientConversation, + tokenList: [], + stats: {} as IStats, }, reducers: { save(state, action) { @@ -60,6 +71,18 @@ const model: DvaModel = { currentConversation: { ...payload, message: messageList }, }; }, + setTokenList(state, { payload }) { + return { + ...state, + tokenList: payload, + }; + }, + setStats(state, { payload }) { + return { + ...state, + stats: payload, + }; + }, }, effects: { @@ -160,6 +183,78 @@ const model: DvaModel = { } return data.retcode; }, + *createToken({ payload }, { call, put }) { + const { data } = yield call(chatService.createToken, payload); + if (data.retcode === 0) { + yield put({ + type: 'listToken', + payload: payload, + }); + message.success(i18n.t('message.created')); + } + return data.retcode; + }, + *listToken({ payload }, { call, put }) { + const { data } = yield call(chatService.listToken, payload); + if (data.retcode === 0) { + yield put({ + type: 'setTokenList', + payload: data.data, + }); + } + return data.retcode; + }, + *removeToken({ payload }, { call, put }) { + const { data } = yield call( + chatService.removeToken, + omit(payload, ['dialogId']), + ); + if (data.retcode === 0) { + yield put({ + type: 'listToken', + payload: { dialog_id: payload.dialogId }, + }); + } + return data.retcode; + }, + *getStats({ payload }, { call, put }) { + const { data } = yield call(chatService.getStats, payload); + if (data.retcode === 0) { + yield put({ + type: 'setStats', + payload: data.data, + }); + } + return data.retcode; + }, + *createExternalConversation({ payload }, { call, put }) { + const { data } = yield call( + chatService.createExternalConversation, + payload, + ); + if (data.retcode === 0) { + yield put({ + type: 'getExternalConversation', + payload: { conversation_id: payload.conversationId }, + }); + } + return data.retcode; + }, + *getExternalConversation({ payload }, { call }) { + const { data } = yield call( + chatService.getExternalConversation, + null, + payload, + ); + return data.retcode; + }, + *completeExternalConversation({ payload }, { call }) { + const { data } = yield call( + chatService.completeExternalConversation, + payload, + ); + return data.retcode; + }, }, }; diff --git a/web/src/services/chatService.ts b/web/src/services/chatService.ts index e2d8d37829..0b4567560e 100644 --- a/web/src/services/chatService.ts +++ b/web/src/services/chatService.ts @@ -12,6 +12,13 @@ const { completeConversation, listConversation, removeConversation, + createToken, + listToken, + removeToken, + getStats, + createExternalConversation, + getExternalConversation, + completeExternalConversation, } = api; const methods = { @@ -51,6 +58,34 @@ const methods = { url: removeConversation, method: 'post', }, + createToken: { + url: createToken, + method: 'post', + }, + listToken: { + url: listToken, + method: 'get', + }, + removeToken: { + url: removeToken, + method: 'post', + }, + getStats: { + url: getStats, + method: 'get', + }, + createExternalConversation: { + url: createExternalConversation, + method: 'post', + }, + getExternalConversation: { + url: getExternalConversation, + method: 'get', + }, + completeExternalConversation: { + url: completeExternalConversation, + method: 'post', + }, } as const; const chatService = registerServer(methods, request); diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index 54b1b1263a..f404eceeae 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -3,7 +3,7 @@ let api_host = `/v1`; export { api_host }; export default { - // 用户 + // user login: `${api_host}/user/login`, logout: `${api_host}/user/logout`, register: `${api_host}/user/register`, @@ -12,21 +12,21 @@ export default { tenant_info: `${api_host}/user/tenant_info`, set_tenant_info: `${api_host}/user/set_tenant_info`, - // 模型管理 + // llm model factories_list: `${api_host}/llm/factories`, llm_list: `${api_host}/llm/list`, my_llm: `${api_host}/llm/my_llms`, set_api_key: `${api_host}/llm/set_api_key`, add_llm: `${api_host}/llm/add_llm`, - //知识库管理 + // knowledge base kb_list: `${api_host}/kb/list`, create_kb: `${api_host}/kb/create`, update_kb: `${api_host}/kb/update`, rm_kb: `${api_host}/kb/rm`, get_kb_detail: `${api_host}/kb/detail`, - // chunk管理 + // chunk chunk_list: `${api_host}/chunk/list`, create_chunk: `${api_host}/chunk/create`, set_chunk: `${api_host}/chunk/set`, @@ -35,7 +35,7 @@ export default { rm_chunk: `${api_host}/chunk/rm`, retrieval_test: `${api_host}/chunk/retrieval_test`, - // 文件管理 + // document upload: `${api_host}/document/upload`, get_document_list: `${api_host}/document/list`, document_change_status: `${api_host}/document/change_status`, @@ -48,14 +48,22 @@ export default { get_document_file: `${api_host}/document/get`, document_upload: `${api_host}/document/upload`, + // chat setDialog: `${api_host}/dialog/set`, getDialog: `${api_host}/dialog/get`, removeDialog: `${api_host}/dialog/rm`, listDialog: `${api_host}/dialog/list`, - setConversation: `${api_host}/conversation/set`, getConversation: `${api_host}/conversation/get`, listConversation: `${api_host}/conversation/list`, removeConversation: `${api_host}/conversation/rm`, completeConversation: `${api_host}/conversation/completion`, + // chat for external + createToken: `${api_host}/api/new_token`, + listToken: `${api_host}/api/token_list`, + removeToken: `${api_host}/api/rm`, + getStats: `${api_host}/api/stats`, + createExternalConversation: `${api_host}/api/new_conversation`, + getExternalConversation: `${api_host}/api/conversation`, + completeExternalConversation: `${api_host}/api/completion`, }; diff --git a/web/src/utils/commonUtil.ts b/web/src/utils/commonUtil.ts new file mode 100644 index 0000000000..9263e99d01 --- /dev/null +++ b/web/src/utils/commonUtil.ts @@ -0,0 +1,17 @@ +import isObject from 'lodash/isObject'; +import snakeCase from 'lodash/snakeCase'; + +export const isFormData = (data: unknown): data is FormData => { + return data instanceof FormData; +}; + +export const convertTheKeysOfTheObjectToSnake = (data: unknown) => { + if (isObject(data) && !isFormData(data)) { + return Object.keys(data).reduce>((pre, cur) => { + const value = (data as Record)[cur]; + pre[isFormData(value) ? cur : snakeCase(cur)] = value; + return pre; + }, {}); + } + return data; +}; diff --git a/web/src/utils/date.ts b/web/src/utils/date.ts index dc9272d301..90aee0aa46 100644 --- a/web/src/utils/date.ts +++ b/web/src/utils/date.ts @@ -1,20 +1,20 @@ -import moment from 'moment'; +import dayjs from 'dayjs'; export function today() { - return formatDate(moment()); + return formatDate(dayjs()); } export function lastDay() { - return formatDate(moment().subtract(1, 'days')); + return formatDate(dayjs().subtract(1, 'days')); } export function lastWeek() { - return formatDate(moment().subtract(1, 'weeks')); + return formatDate(dayjs().subtract(1, 'weeks')); } export function formatDate(date: any) { if (!date) { return ''; } - return moment(date).format('DD/MM/YYYY'); + return dayjs(date).format('DD/MM/YYYY'); } diff --git a/web/src/utils/registerServer.ts b/web/src/utils/registerServer.ts index 9404ce0d42..f36747b2f8 100644 --- a/web/src/utils/registerServer.ts +++ b/web/src/utils/registerServer.ts @@ -8,16 +8,20 @@ const registerServer = ( ) => { const server: Service = {} as Service; for (let key in opt) { - server[key] = (params) => { + server[key] = (params: any, urlAppendix?: string) => { + let url = opt[key].url; + if (urlAppendix) { + url = url + '/' + urlAppendix; + } if (opt[key].method === 'post' || opt[key].method === 'POST') { - return request(opt[key].url, { + return request(url, { method: opt[key].method, data: params, }); } if (opt[key].method === 'get' || opt[key].method === 'GET') { - return request.get(opt[key].url, { + return request.get(url, { params, }); } diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index c5fd867d12..91e7d4b384 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -4,6 +4,7 @@ import authorizationUtil from '@/utils/authorizationUtil'; import { message, notification } from 'antd'; import { history } from 'umi'; import { RequestMethod, extend } from 'umi-request'; +import { convertTheKeysOfTheObjectToSnake } from './commonUtil'; const ABORT_REQUEST_ERR_MESSAGE = 'The user aborted a request.'; // 手动中断请求。errorHandler 抛出的error message @@ -87,10 +88,15 @@ const request: RequestMethod = extend({ request.interceptors.request.use((url: string, options: any) => { const authorization = authorizationUtil.getAuthorization(); + const data = convertTheKeysOfTheObjectToSnake(options.data); + const params = convertTheKeysOfTheObjectToSnake(options.params); + return { url, options: { ...options, + // data, + // params, headers: { ...(options.skipToken ? undefined : { [Authorization]: authorization }), ...options.headers,