From 0fe6821a89f43adedc8c0a5a365c83a9de7a9e21 Mon Sep 17 00:00:00 2001 From: HaHafeng Date: Sun, 16 Nov 2025 15:43:39 +0800 Subject: [PATCH] feat(frontend): add batch processing and review features - Add batch processing API and mode - Add deep read mode for full-text analysis - Add document selector and switcher components - Add review page with editorial and methodology assessment - Add capacity indicator and usage info modal - Add custom hooks for batch tasks and chat modes - Update layouts and routing - Add TypeScript types for chat features --- frontend/package-lock.json | 497 +++++++++++++- frontend/package.json | 9 +- frontend/src/App.tsx | 2 + frontend/src/api/batchApi.ts | 146 ++++ frontend/src/api/chatApi.ts | 3 + frontend/src/api/index.ts | 13 +- frontend/src/api/knowledgeBaseApi.ts | 26 + frontend/src/api/reviewApi.ts | 348 ++++++++++ frontend/src/components/chat/BatchMode.css | 137 ++++ frontend/src/components/chat/BatchMode.tsx | 150 +++++ .../src/components/chat/BatchProgress.css | 140 ++++ .../src/components/chat/BatchProgress.tsx | 129 ++++ frontend/src/components/chat/BatchResults.css | 246 +++++++ frontend/src/components/chat/BatchResults.tsx | 191 ++++++ .../src/components/chat/CapacityIndicator.css | 87 +++ .../src/components/chat/CapacityIndicator.tsx | 93 +++ frontend/src/components/chat/CustomTable.tsx | 142 ++++ frontend/src/components/chat/DeepReadMode.css | 101 +++ frontend/src/components/chat/DeepReadMode.tsx | 103 +++ .../src/components/chat/DocumentSelection.css | 134 ++++ .../src/components/chat/DocumentSelection.tsx | 156 +++++ .../src/components/chat/DocumentSelector.css | 202 ++++++ .../src/components/chat/DocumentSelector.tsx | 129 ++++ .../src/components/chat/DocumentSwitcher.css | 101 +++ .../src/components/chat/DocumentSwitcher.tsx | 60 ++ frontend/src/components/chat/FullTextMode.css | 94 +++ frontend/src/components/chat/FullTextMode.tsx | 73 ++ .../components/chat/FullTextModeHeader.css | 107 +++ .../components/chat/FullTextModeHeader.tsx | 73 ++ frontend/src/components/chat/MessageList.tsx | 75 ++- frontend/src/components/chat/ModeSelector.css | 104 +++ frontend/src/components/chat/ModeSelector.tsx | 114 ++++ .../src/components/chat/ModelSelector.tsx | 19 +- frontend/src/components/chat/PresetTable.tsx | 150 +++++ .../src/components/chat/TaskDefinition.css | 150 +++++ .../src/components/chat/TaskDefinition.tsx | 98 +++ .../src/components/chat/UsageInfoModal.css | 180 +++++ .../src/components/chat/UsageInfoModal.tsx | 100 +++ .../src/components/review/EditorialReview.tsx | 192 ++++++ .../components/review/MethodologyReview.tsx | 207 ++++++ frontend/src/components/review/ScoreCard.tsx | 122 ++++ frontend/src/hooks/useBatchTask.ts | 197 ++++++ frontend/src/hooks/useChatMode.ts | 82 +++ frontend/src/hooks/useDeepReadState.ts | 81 +++ frontend/src/layouts/MainLayout.tsx | 6 + frontend/src/pages/ChatPage.backup.tsx | 179 +++++ frontend/src/pages/ChatPage.new.tsx | 387 +++++++++++ frontend/src/pages/ChatPage.tsx | 459 ++++++++++--- frontend/src/pages/ReviewPage.css | 121 ++++ frontend/src/pages/ReviewPage.tsx | 624 ++++++++++++++++++ frontend/src/types/chat.ts | 91 +++ frontend/vite.config.ts | 3 + 52 files changed, 7324 insertions(+), 109 deletions(-) create mode 100644 frontend/src/api/batchApi.ts create mode 100644 frontend/src/api/reviewApi.ts create mode 100644 frontend/src/components/chat/BatchMode.css create mode 100644 frontend/src/components/chat/BatchMode.tsx create mode 100644 frontend/src/components/chat/BatchProgress.css create mode 100644 frontend/src/components/chat/BatchProgress.tsx create mode 100644 frontend/src/components/chat/BatchResults.css create mode 100644 frontend/src/components/chat/BatchResults.tsx create mode 100644 frontend/src/components/chat/CapacityIndicator.css create mode 100644 frontend/src/components/chat/CapacityIndicator.tsx create mode 100644 frontend/src/components/chat/CustomTable.tsx create mode 100644 frontend/src/components/chat/DeepReadMode.css create mode 100644 frontend/src/components/chat/DeepReadMode.tsx create mode 100644 frontend/src/components/chat/DocumentSelection.css create mode 100644 frontend/src/components/chat/DocumentSelection.tsx create mode 100644 frontend/src/components/chat/DocumentSelector.css create mode 100644 frontend/src/components/chat/DocumentSelector.tsx create mode 100644 frontend/src/components/chat/DocumentSwitcher.css create mode 100644 frontend/src/components/chat/DocumentSwitcher.tsx create mode 100644 frontend/src/components/chat/FullTextMode.css create mode 100644 frontend/src/components/chat/FullTextMode.tsx create mode 100644 frontend/src/components/chat/FullTextModeHeader.css create mode 100644 frontend/src/components/chat/FullTextModeHeader.tsx create mode 100644 frontend/src/components/chat/ModeSelector.css create mode 100644 frontend/src/components/chat/ModeSelector.tsx create mode 100644 frontend/src/components/chat/PresetTable.tsx create mode 100644 frontend/src/components/chat/TaskDefinition.css create mode 100644 frontend/src/components/chat/TaskDefinition.tsx create mode 100644 frontend/src/components/chat/UsageInfoModal.css create mode 100644 frontend/src/components/chat/UsageInfoModal.tsx create mode 100644 frontend/src/components/review/EditorialReview.tsx create mode 100644 frontend/src/components/review/MethodologyReview.tsx create mode 100644 frontend/src/components/review/ScoreCard.tsx create mode 100644 frontend/src/hooks/useBatchTask.ts create mode 100644 frontend/src/hooks/useChatMode.ts create mode 100644 frontend/src/hooks/useDeepReadState.ts create mode 100644 frontend/src/pages/ChatPage.backup.tsx create mode 100644 frontend/src/pages/ChatPage.new.tsx create mode 100644 frontend/src/pages/ReviewPage.css create mode 100644 frontend/src/pages/ReviewPage.tsx create mode 100644 frontend/src/types/chat.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 33c18990..b81d9de2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -13,13 +13,17 @@ "@types/react-syntax-highlighter": "^15.5.13", "antd": "^5.22.5", "axios": "^1.12.2", + "html2canvas": "^1.4.1", "js-yaml": "^4.1.0", + "jspdf": "^3.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-router-dom": "^6.28.0", "react-syntax-highlighter": "^15.6.6", - "remark-gfm": "^4.0.1" + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@types/react": "^18.3.18", @@ -1848,12 +1852,25 @@ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", "license": "MIT" }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz", "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "18.3.26", "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.26.tgz", @@ -1883,6 +1900,13 @@ "@types/react": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmmirror.com/@types/unist/-/unist-3.0.3.tgz", @@ -2174,6 +2198,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", @@ -2391,6 +2424,15 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.8.15", "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.15.tgz", @@ -2525,6 +2567,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmmirror.com/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/ccount/-/ccount-2.0.1.tgz", @@ -2535,6 +2597,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -2636,6 +2711,15 @@ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -2717,6 +2801,30 @@ "toggle-selection": "^1.0.6" } }, + "node_modules/core-js": { + "version": "3.46.0", + "resolved": "https://registry.npmmirror.com/core-js/-/core-js-3.46.0.tgz", + "integrity": "sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2732,6 +2840,15 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", @@ -2839,6 +2956,16 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.3.0", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2874,6 +3001,18 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3289,6 +3428,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmmirror.com/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", @@ -3312,6 +3462,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3437,6 +3593,15 @@ "node": ">=0.4.x" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3637,6 +3802,56 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5/node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-parse-selector": { "version": "2.2.5", "resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", @@ -3647,6 +3862,31 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -3674,6 +3914,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmmirror.com/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -3777,6 +4046,29 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/ignore": { "version": "7.0.5", "resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", @@ -3820,6 +4112,12 @@ "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", "license": "MIT" }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmmirror.com/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -4055,6 +4353,23 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/jspdf/-/jspdf-3.0.3.tgz", + "integrity": "sha512-eURjAyz5iX1H8BOYAfzvdPfIKK53V7mCpBTe7Kb16PaM8JSXEcUQNBQaiWMI8wY5RvNOPj4GccMjTlfwRBd+oQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.9", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.2.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", @@ -5251,6 +5566,12 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", @@ -5289,6 +5610,18 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", @@ -5340,6 +5673,13 @@ "dev": true, "license": "ISC" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", @@ -5609,6 +5949,16 @@ ], "license": "MIT" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmmirror.com/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/rc-cascader": { "version": "3.34.0", "resolved": "https://registry.npmmirror.com/rc-cascader/-/rc-cascader-3.34.0.tgz", @@ -6471,6 +6821,28 @@ "node": ">=6" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -6585,6 +6957,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rollup": { "version": "4.52.4", "resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.52.4.tgz", @@ -6738,6 +7120,28 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/string-convert": { "version": "0.2.1", "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", @@ -6948,6 +7352,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tailwindcss": { "version": "3.4.18", "resolved": "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.18.tgz", @@ -6986,6 +7400,15 @@ "node": ">=14.0.0" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz", @@ -7287,6 +7710,15 @@ "dev": true, "license": "MIT" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmmirror.com/vfile/-/vfile-6.0.3.tgz", @@ -7301,6 +7733,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/vfile-message": { "version": "4.0.3", "resolved": "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz", @@ -7421,6 +7867,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", @@ -7437,6 +7893,24 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", @@ -7542,6 +8016,27 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index e138b911..70fb4c3d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,13 +15,17 @@ "@types/react-syntax-highlighter": "^15.5.13", "antd": "^5.22.5", "axios": "^1.12.2", + "html2canvas": "^1.4.1", "js-yaml": "^4.1.0", + "jspdf": "^3.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", "react-markdown": "^10.1.0", "react-router-dom": "^6.28.0", "react-syntax-highlighter": "^15.6.6", - "remark-gfm": "^4.0.1" + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@types/react": "^18.3.18", @@ -39,6 +43,3 @@ "vite": "^6.0.7" } } - - - diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ef999d33..1e194ff7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,6 +5,7 @@ import AgentChatPage from './pages/AgentChatPage' import ChatPage from './pages/ChatPage' import KnowledgePage from './pages/KnowledgePage' import HistoryPage from './pages/HistoryPage' +import ReviewPage from './pages/ReviewPage' function App() { return ( @@ -12,6 +13,7 @@ function App() { }> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/api/batchApi.ts b/frontend/src/api/batchApi.ts new file mode 100644 index 00000000..2b3d021b --- /dev/null +++ b/frontend/src/api/batchApi.ts @@ -0,0 +1,146 @@ +/** + * Phase 3: 批处理模式 - API封装 + */ + +import request from './request'; + +// ==================== 类型定义 ==================== + +export interface BatchTemplate { + id: string; + name: string; + description: string; + output_fields: Array<{ + key: string; + label: string; + type: string; + description?: string; + }>; +} + +export interface ExecuteBatchParams { + kb_id: string; + document_ids: string[]; + template_type: 'preset' | 'custom'; + template_id?: string; + custom_prompt?: string; + model_type: string; + task_name?: string; +} + +export interface BatchTask { + id: string; + name: string; + status: 'processing' | 'completed' | 'failed'; + total_documents: number; + completed_count: number; + failed_count: number; + model_type: string; + started_at: string; + completed_at?: string; + duration_seconds?: number; + created_at: string; +} + +export interface BatchResult { + id: string; + index: number; + document_id: string; + document_name: string; + status: 'success' | 'failed'; + data: any; + raw_output?: string; + error_message?: string; + processing_time_ms?: number; + tokens_used?: number; + created_at: string; +} + +export interface BatchTaskWithResults { + task: BatchTask; + results: BatchResult[]; +} + +// ==================== API函数 ==================== + +/** + * 获取所有预设模板 + */ +export async function getTemplates(): Promise { + const response = await request.get('/batch/templates'); + return response.data; +} + +/** + * 执行批处理任务 + */ +export async function executeBatch(params: ExecuteBatchParams): Promise<{ + task_id: string; + status: string; + websocket_event: string; +}> { + const response = await request.post('/batch/execute', params); + return response.data; +} + +/** + * 获取任务状态 + */ +export async function getTask(taskId: string): Promise { + const response = await request.get(`/batch/tasks/${taskId}`); + return response.data; +} + +/** + * 获取任务结果 + */ +export async function getTaskResults(taskId: string): Promise { + const response = await request.get(`/batch/tasks/${taskId}/results`); + return response.data; +} + +/** + * 重试失败的文档 + */ +export async function retryFailed(taskId: string): Promise { + await request.post(`/batch/tasks/${taskId}/retry-failed`); +} + +/** + * 轮询任务状态直到完成 + */ +export async function pollTaskUntilComplete( + taskId: string, + onProgress?: (task: BatchTask) => void, + maxAttempts: number = 120, // 最多10分钟(120次 * 5秒) +): Promise { + let attempts = 0; + + while (attempts < maxAttempts) { + const task = await getTask(taskId); + + if (onProgress) { + onProgress(task); + } + + if (task.status === 'completed' || task.status === 'failed') { + return task; + } + + // 等待5秒后再次查询 + await new Promise(resolve => setTimeout(resolve, 5000)); + attempts++; + } + + throw new Error('任务执行超时'); +} + +export default { + getTemplates, + executeBatch, + getTask, + getTaskResults, + retryFailed, + pollTaskUntilComplete, +}; + diff --git a/frontend/src/api/chatApi.ts b/frontend/src/api/chatApi.ts index f4181125..c7f25478 100644 --- a/frontend/src/api/chatApi.ts +++ b/frontend/src/api/chatApi.ts @@ -26,6 +26,8 @@ export interface SendChatMessageData { content: string modelType: string knowledgeBaseIds?: string[] + documentIds?: string[] // Phase 2: 逐篇精读模式 - 限定文档范围(RAG检索) + fullTextDocumentIds?: string[] // Phase 2: 全文阅读模式 - 传递完整全文 conversationId?: string } @@ -137,3 +139,4 @@ export default { deleteConversation, } + diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 83fa0ac7..eaa09a63 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -10,11 +10,14 @@ export const getApiInfo = () => { return request.get('/') } -// TODO: Day 9+ 添加更多API -// - 项目管理API -// - 对话API -// - 知识库API -// - 用户API +// 导出各模块API +export * from './projectApi' +export * from './conversationApi' +export * from './knowledgeBaseApi' +export * from './agentApi' +export * from './chatApi' +export * from './batchApi' +export * from './reviewApi' export default { healthCheck, diff --git a/frontend/src/api/knowledgeBaseApi.ts b/frontend/src/api/knowledgeBaseApi.ts index 8baa64f6..84b6a2f0 100644 --- a/frontend/src/api/knowledgeBaseApi.ts +++ b/frontend/src/api/knowledgeBaseApi.ts @@ -181,6 +181,32 @@ export const documentApi = { async reprocess(id: string): Promise { await api.post(`/documents/${id}/reprocess`); }, + + /** + * Phase 2: 获取文档全文(用于逐篇精读模式) + */ + async getFullText(id: string): Promise { + const response = await api.get(`/documents/${id}/full-text`); + return response.data; + }, +}; + +/** + * Phase 2: 文档选择API(用于全文阅读模式) + */ +export const documentSelectionApi = { + /** + * 获取知识库的文档选择结果 + */ + async getSelection(kbId: string, maxFiles?: number, maxTokens?: number): Promise { + const params = new URLSearchParams(); + if (maxFiles) params.append('max_files', maxFiles.toString()); + if (maxTokens) params.append('max_tokens', maxTokens.toString()); + + const url = `/knowledge-bases/${kbId}/document-selection${params.toString() ? '?' + params.toString() : ''}`; + const response = await api.get(url); + return response.data.data; // 修复:返回内层的data对象 + }, }; diff --git a/frontend/src/api/reviewApi.ts b/frontend/src/api/reviewApi.ts new file mode 100644 index 00000000..86eb84ec --- /dev/null +++ b/frontend/src/api/reviewApi.ts @@ -0,0 +1,348 @@ +/** + * 稿件审查功能 - API封装 + */ + +import request from './request'; +import axios from 'axios'; + +// ==================== 类型定义 ==================== + +/** + * 稿约规范性评估 - 单项 + */ +export interface EditorialItem { + criterion: string; + status: 'pass' | 'warning' | 'fail'; + score: number; + issues: string[]; + suggestions: string[]; +} + +/** + * 稿约规范性评估结果 + */ +export interface EditorialReview { + overall_score: number; + summary: string; + items: EditorialItem[]; +} + +/** + * 方法学评估 - 问题项 + */ +export interface MethodologyIssue { + type: string; + severity: 'major' | 'minor'; + description: string; + location: string; + suggestion: string; +} + +/** + * 方法学评估 - 部分 + */ +export interface MethodologyPart { + part: string; + score: number; + issues: MethodologyIssue[]; +} + +/** + * 方法学评估结果 + */ +export interface MethodologyReview { + overall_score: number; + summary: string; + parts: MethodologyPart[]; +} + +/** + * 审查任务状态 + */ +export type ReviewTaskStatus = + | 'pending' // 等待处理 + | 'extracting' // 提取文档文本 + | 'reviewing_editorial' // 稿约规范性评估 + | 'reviewing_methodology' // 方法学评估 + | 'completed' // 完成 + | 'failed'; // 失败 + +/** + * 审查任务 + */ +export interface ReviewTask { + id: string; + fileName: string; + fileSize: number; + status: ReviewTaskStatus; + wordCount?: number; + overallScore?: number; + modelUsed?: string; + createdAt: string; + startedAt?: string; + completedAt?: string; + durationSeconds?: number; + errorMessage?: string; +} + +/** + * 完整审查报告 + */ +export interface ReviewReport { + taskId: string; + fileName: string; + wordCount?: number; + modelUsed?: string; + overallScore?: number; + editorialReview?: EditorialReview; + methodologyReview?: MethodologyReview; + completedAt?: string; + durationSeconds?: number; +} + +/** + * 任务列表项(简化版) + */ +export interface ReviewTaskListItem { + id: string; + fileName: string; + fileSize: number; + status: ReviewTaskStatus; + overallScore?: number; + modelUsed?: string; + createdAt: string; + completedAt?: string; + durationSeconds?: number; + wordCount?: number; +} + +/** + * 任务列表响应 + */ +export interface ReviewTaskListResponse { + tasks: ReviewTaskListItem[]; + pagination: { + page: number; + limit: number; + total: number; + totalPages: number; + }; +} + +/** + * 上传参数 + */ +export interface UploadManuscriptParams { + file: File; + modelType?: 'deepseek-v3' | 'qwen3-72b' | 'qwen-long'; +} + +// ==================== API函数 ==================== + +/** + * 上传稿件并开始审查 + */ +export async function uploadManuscript(params: UploadManuscriptParams): Promise<{ + taskId: string; + fileName: string; + status: ReviewTaskStatus; + createdAt: string; +}> { + const formData = new FormData(); + formData.append('file', params.file); + formData.append('modelType', params.modelType || 'deepseek-v3'); + + // 使用axios直接调用,因为FormData需要特殊处理 + const response = await axios.post('/api/v1/review/upload', formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + timeout: 30000, + }); + + return response.data.data; +} + +/** + * 获取任务状态 + */ +export async function getTaskStatus(taskId: string): Promise { + const response = await request.get(`/review/tasks/${taskId}`); + return response.data; +} + +/** + * 获取审查报告(完整) + */ +export async function getTaskReport(taskId: string): Promise { + const response = await request.get(`/review/tasks/${taskId}/report`); + return response.data; +} + +/** + * 获取任务列表 + */ +export async function getTaskList(page: number = 1, limit: number = 20): Promise { + const response = await request.get('/review/tasks', { + params: { page, limit }, + }); + + return { + tasks: response.data, + pagination: response.pagination, + }; +} + +/** + * 删除任务 + */ +export async function deleteTask(taskId: string): Promise { + await request.delete(`/review/tasks/${taskId}`); +} + +/** + * 轮询任务状态直到完成 + * @param taskId 任务ID + * @param onProgress 进度回调 + * @param maxAttempts 最大尝试次数(默认60次,即5分钟) + * @returns 完成的任务 + */ +export async function pollTaskUntilComplete( + taskId: string, + onProgress?: (task: ReviewTask) => void, + maxAttempts: number = 60, +): Promise { + let attempts = 0; + + while (attempts < maxAttempts) { + const task = await getTaskStatus(taskId); + + if (onProgress) { + onProgress(task); + } + + if (task.status === 'completed' || task.status === 'failed') { + return task; + } + + // 等待5秒后再次查询 + await new Promise(resolve => setTimeout(resolve, 5000)); + attempts++; + } + + throw new Error('任务执行超时(超过5分钟)'); +} + +/** + * 获取任务状态的中文描述 + */ +export function getStatusText(status: ReviewTaskStatus): string { + const statusMap: Record = { + pending: '等待处理', + extracting: '提取文档文本', + reviewing_editorial: '稿约规范性评估', + reviewing_methodology: '方法学评估', + completed: '评估完成', + failed: '评估失败', + }; + return statusMap[status] || status; +} + +/** + * 获取任务状态的颜色 + */ +export function getStatusColor(status: ReviewTaskStatus): string { + const colorMap: Record = { + pending: 'default', + extracting: 'processing', + reviewing_editorial: 'processing', + reviewing_methodology: 'processing', + completed: 'success', + failed: 'error', + }; + return colorMap[status] || 'default'; +} + +/** + * 格式化文件大小 + */ +export function formatFileSize(bytes: number): string { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +/** + * 格式化时长 + */ +export function formatDuration(seconds: number): string { + if (seconds < 60) return `${seconds}秒`; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}分${remainingSeconds}秒`; +} + +/** + * 获取评分等级 + */ +export function getScoreLevel(score: number): { + level: 'excellent' | 'good' | 'fair' | 'poor'; + text: string; + color: string; +} { + if (score >= 90) { + return { level: 'excellent', text: '优秀', color: '#52c41a' }; + } else if (score >= 80) { + return { level: 'good', text: '良好', color: '#1890ff' }; + } else if (score >= 60) { + return { level: 'fair', text: '及格', color: '#faad14' }; + } else { + return { level: 'poor', text: '不及格', color: '#f5222d' }; + } +} + +// 默认导出 +export default { + uploadManuscript, + getTaskStatus, + getTaskReport, + getTaskList, + deleteTask, + pollTaskUntilComplete, + getStatusText, + getStatusColor, + formatFileSize, + formatDuration, + getScoreLevel, +}; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/chat/BatchMode.css b/frontend/src/components/chat/BatchMode.css new file mode 100644 index 00000000..202d0992 --- /dev/null +++ b/frontend/src/components/chat/BatchMode.css @@ -0,0 +1,137 @@ +/* Phase 3: 批处理模式 - 主组件样式 */ + +.batch-mode { + display: flex; + flex-direction: column; /* 改成垂直布局 */ + gap: 24px; + overflow-y: auto; /* 允许滚动 */ + padding: 10px; + max-width: 1600px; /* 限制最大宽度 */ + margin: 0; /* 靠左对齐 */ +} + +/* 每个步骤区块 */ +.batch-section { + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 8px; + padding: 20px; +} + +/* 步骤标题 */ +.section-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 2px solid #f0f0f0; +} + +.section-number { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: #1890ff; + color: #fff; + border-radius: 50%; + font-weight: 600; + font-size: 16px; + flex-shrink: 0; +} + +.section-title { + font-size: 18px; + font-weight: 600; + color: #262626; +} + +/* 占位符(初始状态) */ +.batch-placeholder { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 40px; + background: #fafafa; + border: 2px dashed #d9d9d9; + border-radius: 8px; +} + +.placeholder-icon { + font-size: 64px; + margin-bottom: 16px; +} + +.placeholder-title { + font-size: 20px; + font-weight: 600; + color: #262626; + margin-bottom: 8px; +} + +.placeholder-desc { + font-size: 14px; + color: #8c8c8c; + margin-bottom: 32px; +} + +.placeholder-steps { + display: flex; + flex-direction: column; + gap: 16px; + width: 100%; + max-width: 400px; +} + +.step-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + background: #fff; + border-radius: 6px; + border: 1px solid #e8e8e8; +} + +.step-number { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + background: #1890ff; + color: #fff; + border-radius: 50%; + font-weight: 600; + font-size: 14px; + flex-shrink: 0; +} + +.step-text { + font-size: 14px; + color: #595959; +} + +/* 自定义滚动条 */ +.batch-left-panel::-webkit-scrollbar { + width: 6px; +} + +.batch-left-panel::-webkit-scrollbar-track { + background: #f0f0f0; + border-radius: 3px; +} + +.batch-left-panel::-webkit-scrollbar-thumb { + background: #bfbfbf; + border-radius: 3px; +} + +.batch-left-panel::-webkit-scrollbar-thumb:hover { + background: #8c8c8c; +} + diff --git a/frontend/src/components/chat/BatchMode.tsx b/frontend/src/components/chat/BatchMode.tsx new file mode 100644 index 00000000..6e6faae7 --- /dev/null +++ b/frontend/src/components/chat/BatchMode.tsx @@ -0,0 +1,150 @@ +/** + * Phase 3: 批处理模式 - 主组件 + */ + +import { useEffect, useState } from 'react'; +import { Button, message as antdMessage } from 'antd'; +import { RocketOutlined } from '@ant-design/icons'; +import { TaskDefinition } from './TaskDefinition'; +import { DocumentSelection } from './DocumentSelection'; +import { BatchProgress } from './BatchProgress'; +import { BatchResults } from './BatchResults'; +import { useBatchTask } from '../../hooks/useBatchTask'; +import { documentApi, type Document } from '../../api/knowledgeBaseApi'; +import './BatchMode.css'; + +interface BatchModeProps { + kbId: string; + modelType: string; +} + +export function BatchMode({ kbId, modelType }: BatchModeProps) { + const [documents, setDocuments] = useState([]); + + const { + state, + loadTemplates, + selectTemplate, + setCustomPrompt, + setSelectedDocuments, + executeBatch, + retryFailed, + reset, + } = useBatchTask(); + + // 加载模板和文档列表 + useEffect(() => { + loadTemplates(); + loadDocuments(); + }, [kbId]); + + // 加载知识库文档列表 + const loadDocuments = async () => { + try { + const docs = await documentApi.getList(kbId); + setDocuments(docs); + } catch (error: any) { + antdMessage.error(`加载文献列表失败: ${error.message}`); + } + }; + + // 开始批处理 + const handleStartBatch = async () => { + if (state.selectedDocuments.length < 3) { + antdMessage.warning('请至少选择3篇文献'); + return; + } + + if (state.selectedDocuments.length > 50) { + antdMessage.warning('最多只能选择50篇文献'); + return; + } + + await executeBatch({ + kbId, + modelType, + taskName: state.selectedTemplate + ? `${state.selectedTemplate.name}_${new Date().toLocaleString('zh-CN')}` + : `自定义任务_${new Date().toLocaleString('zh-CN')}`, + }); + }; + + return ( +
+ {/* 步骤1:任务定义 */} +
+
+ 1 + 定义任务 +
+ +
+ + {/* 步骤2:选择文献 */} +
+
+ 2 + 选择文献 +
+ +
+ + {/* 步骤3:开始按钮 */} + {state.step === 'define' && ( +
+ +
+ )} + + {/* 步骤3:执行进度 */} + {state.step === 'executing' && state.currentTask && ( +
+
+ 3 + 执行中... +
+ +
+ )} + + {/* 步骤4:结果展示 */} + {state.step === 'results' && state.currentTask && ( +
+
+ 4 + 结果展示 +
+ +
+ )} +
+ ); +} + diff --git a/frontend/src/components/chat/BatchProgress.css b/frontend/src/components/chat/BatchProgress.css new file mode 100644 index 00000000..78c4970d --- /dev/null +++ b/frontend/src/components/chat/BatchProgress.css @@ -0,0 +1,140 @@ +/* Phase 3: 批处理模式 - 执行进度样式 */ + +.batch-progress { + padding: 24px; + background: #fff; + border-radius: 8px; + border: 1px solid #e8e8e8; +} + +.progress-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; +} + +.progress-title { + font-size: 16px; + font-weight: 600; + color: #262626; +} + +.progress-bar { + margin-bottom: 20px; +} + +.progress-text { + text-align: center; + margin-top: 8px; + font-size: 14px; + color: #595959; + font-weight: 500; +} + +.progress-stats { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 12px; + margin-bottom: 20px; +} + +.stat-item { + padding: 12px; + border-radius: 6px; + text-align: center; + border: 1px solid #e8e8e8; +} + +.stat-item.stat-success { + background: #f6ffed; + border-color: #b7eb8f; +} + +.stat-item.stat-failed { + background: #fff2f0; + border-color: #ffccc7; +} + +.stat-item.stat-processing { + background: #e6f7ff; + border-color: #91d5ff; +} + +.stat-item.stat-pending { + background: #fafafa; + border-color: #d9d9d9; +} + +.stat-label { + font-size: 13px; + color: #8c8c8c; + margin-bottom: 4px; +} + +.stat-value { + font-size: 24px; + font-weight: 600; + color: #262626; +} + +.progress-time { + display: flex; + gap: 24px; + padding: 12px; + background: #fafafa; + border-radius: 6px; + margin-bottom: 16px; +} + +.time-item { + display: flex; + align-items: center; + gap: 6px; +} + +.time-label { + font-size: 13px; + color: #8c8c8c; +} + +.time-value { + font-size: 14px; + font-weight: 500; + color: #262626; +} + +.task-info { + padding: 12px; + background: #f5f5f5; + border-radius: 6px; +} + +.info-row { + display: flex; + gap: 8px; + margin-bottom: 6px; + font-size: 13px; +} + +.info-row:last-child { + margin-bottom: 0; +} + +.info-label { + color: #8c8c8c; +} + +.info-value { + color: #262626; + font-weight: 500; +} + + + + + + + + + diff --git a/frontend/src/components/chat/BatchProgress.tsx b/frontend/src/components/chat/BatchProgress.tsx new file mode 100644 index 00000000..e73a807f --- /dev/null +++ b/frontend/src/components/chat/BatchProgress.tsx @@ -0,0 +1,129 @@ +/** + * Phase 3: 批处理模式 - 执行进度组件 + */ + +import React from 'react'; +import { Progress, Spin } from 'antd'; +import type { BatchTask } from '../../api/batchApi'; +import './BatchProgress.css'; + +interface BatchProgressProps { + task: BatchTask; +} + +export function BatchProgress({ task }: BatchProgressProps) { + const percent = task.total_documents > 0 + ? Math.round((task.completed_count / task.total_documents) * 100) + : 0; + + const successCount = task.completed_count - task.failed_count; + const processingCount = task.status === 'processing' ? 1 : 0; + const pendingCount = task.total_documents - task.completed_count - processingCount; + + return ( +
+
+ + + {task.status === 'processing' ? '批处理执行中...' : '批处理已完成'} + +
+ + {/* 进度条 */} +
+ +
+ {task.completed_count} / {task.total_documents} 篇 +
+
+ + {/* 统计信息 */} +
+
+
✅ 成功
+
{successCount}
+
+
+
❌ 失败
+
{task.failed_count}
+
+
+
🔄 处理中
+
{processingCount}
+
+
+
⏳ 等待
+
{pendingCount}
+
+
+ + {/* 时间信息 */} +
+ {task.duration_seconds ? ( +
+ 总用时: + {formatDuration(task.duration_seconds)} +
+ ) : ( +
+ 已用时: + + {formatDuration(Math.floor((Date.now() - new Date(task.started_at).getTime()) / 1000))} + +
+ )} + + {task.status === 'processing' && task.completed_count > 0 && ( +
+ 平均速度: + + {Math.round((Date.now() - new Date(task.started_at).getTime()) / task.completed_count / 1000)}秒/篇 + +
+ )} +
+ + {/* 任务信息 */} +
+
+ 任务名称: + {task.name} +
+
+ 使用模型: + {task.model_type} +
+
+
+ ); +} + +/** + * 格式化时长 + */ +function formatDuration(seconds: number): string { + if (seconds < 60) { + return `${seconds}秒`; + } + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + + return `${minutes}分${remainingSeconds}秒`; +} + + + + + + + + + diff --git a/frontend/src/components/chat/BatchResults.css b/frontend/src/components/chat/BatchResults.css new file mode 100644 index 00000000..a452eee8 --- /dev/null +++ b/frontend/src/components/chat/BatchResults.css @@ -0,0 +1,246 @@ +/* Phase 3: 批处理模式 - 结果展示样式 */ + +.batch-results { + display: flex; + flex-direction: column; + background: #fff; +} + +.results-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 20px; + border-bottom: 1px solid #e8e8e8; +} + +.results-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: #262626; +} + +.title-icon { + font-size: 20px; +} + +.results-summary { + display: flex; + gap: 24px; + padding: 16px 20px; + background: #fafafa; + border-bottom: 1px solid #e8e8e8; + flex-wrap: wrap; +} + +.summary-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; +} + +.summary-label { + color: #8c8c8c; +} + +.summary-value { + color: #262626; + font-weight: 500; +} + +.summary-value.success-text { + color: #52c41a; +} + +.summary-value.failed-text { + color: #ff4d4f; +} + +.results-table { + margin-top: 20px; + overflow-x: auto; /* 允许横向滚动 */ + width: 100%; +} + +/* 预设表格样式 */ +.preset-table { + width: 100%; + overflow: visible; /* 让表格自己处理滚动 */ +} + +.preset-table .ant-table { + font-size: 13px; +} + +.preset-table .ant-table-container { + overflow-x: auto !important; /* 强制横向滚动 */ +} + +/* Ant Design 表格内部滚动条美化 */ +.preset-table .ant-table-body::-webkit-scrollbar, +.preset-table .ant-table-container::-webkit-scrollbar { + height: 10px; + width: 10px; +} + +.preset-table .ant-table-body::-webkit-scrollbar-track, +.preset-table .ant-table-container::-webkit-scrollbar-track { + background: #f0f0f0; + border-radius: 5px; +} + +.preset-table .ant-table-body::-webkit-scrollbar-thumb, +.preset-table .ant-table-container::-webkit-scrollbar-thumb { + background: #bfbfbf; + border-radius: 5px; +} + +.preset-table .ant-table-body::-webkit-scrollbar-thumb:hover, +.preset-table .ant-table-container::-webkit-scrollbar-thumb:hover { + background: #8c8c8c; +} + +.preset-table .ant-table-thead > tr > th { + background: #fafafa; + font-weight: 600; + padding: 12px 8px; +} + +.preset-table .ant-table-tbody > tr > td { + padding: 12px 8px; + vertical-align: top; /* 顶部对齐,适合多行内容 */ +} + +.preset-table .table-cell-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* 新增:支持换行的单元格 */ +.preset-table .table-cell-wrap { + white-space: pre-wrap; /* 保留换行,自动换行 */ + word-break: break-word; /* 允许单词内断行 */ + line-height: 1.6; + font-size: 13px; + color: #262626; + max-height: 200px; /* 限制最大高度 */ + overflow-y: auto; /* 超出显示滚动条 */ + padding: 4px 0; +} + +/* 单元格内滚动条美化 */ +.preset-table .table-cell-wrap::-webkit-scrollbar { + width: 4px; +} + +.preset-table .table-cell-wrap::-webkit-scrollbar-track { + background: #f0f0f0; + border-radius: 2px; +} + +.preset-table .table-cell-wrap::-webkit-scrollbar-thumb { + background: #bfbfbf; + border-radius: 2px; +} + +.preset-table .table-cell-wrap::-webkit-scrollbar-thumb:hover { + background: #8c8c8c; +} + +.preset-table .doc-name { + font-weight: 500; + word-break: break-all; /* 文件名可以断行 */ + line-height: 1.4; +} + +/* 紧凑型文献名称显示 */ +.preset-table .doc-name-compact { + font-weight: 500; + font-size: 12px; + line-height: 1.3; + color: #595959; + display: -webkit-box; + -webkit-line-clamp: 2; /* 最多显示2行 */ + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + word-break: break-all; + max-height: 32px; /* 约2行的高度 */ +} + +/* 自定义表格样式 */ +.custom-table .doc-name { + font-weight: 500; + color: #262626; +} + +.custom-table .result-text { + font-size: 13px; + line-height: 1.6; + color: #595959; +} + +.custom-table .text-preview { + white-space: pre-wrap; + word-break: break-word; +} + +.custom-table .text-ellipsis { + color: #bfbfbf; + margin: 0 4px; +} + +.custom-table .view-full-link { + color: #1890ff; + margin-left: 8px; + cursor: pointer; +} + +.custom-table .view-full-link:hover { + text-decoration: underline; +} + +.custom-table .error-message { + color: #ff4d4f; + font-size: 13px; +} + +/* 详情模态框 */ +.result-detail-modal .result-detail { + max-height: 60vh; + overflow-y: auto; +} + +.result-detail .detail-section { + margin-bottom: 16px; +} + +.result-detail .section-title { + font-weight: 600; + color: #262626; + margin-bottom: 8px; +} + +.result-detail .section-content { + font-size: 14px; + line-height: 1.8; + color: #595959; + white-space: pre-wrap; + word-break: break-word; + padding: 12px; + background: #fafafa; + border-radius: 4px; +} + +.result-detail .detail-meta { + display: flex; + gap: 8px; + padding-top: 12px; + border-top: 1px solid #e8e8e8; +} + diff --git a/frontend/src/components/chat/BatchResults.tsx b/frontend/src/components/chat/BatchResults.tsx new file mode 100644 index 00000000..58811910 --- /dev/null +++ b/frontend/src/components/chat/BatchResults.tsx @@ -0,0 +1,191 @@ +/** + * Phase 3: 批处理模式 - 结果展示主组件 + */ + +import React from 'react'; +import { Button, Space, message } from 'antd'; +import { DownloadOutlined, ReloadOutlined, RollbackOutlined } from '@ant-design/icons'; +import { PresetTable } from './PresetTable'; +import { CustomTable } from './CustomTable'; +import type { BatchTask, BatchResult, BatchTemplate } from '../../api/batchApi'; +import './BatchResults.css'; + +interface BatchResultsProps { + task: BatchTask; + results: BatchResult[]; + template: BatchTemplate | null; + onRetryFailed: () => void; + onReset: () => void; +} + +export function BatchResults({ + task, + results, + template, + onRetryFailed, + onReset, +}: BatchResultsProps) { + // 导出Excel + const handleExportExcel = async () => { + try { + // 动态导入xlsx + const XLSX = await import('xlsx'); + + if (template) { + // 预设模板:8列表格 + const exportData = results + .filter(r => r.status === 'success') + .map(r => ({ + '序号': r.index, + '文献名称': r.document_name, + '研究目的': r.data?.research_purpose || '', + '研究设计': r.data?.research_design || '', + '研究对象': r.data?.research_subjects || '', + '样本量': r.data?.sample_size || '', + '干预组': r.data?.intervention_group || '', + '对照组': r.data?.control_group || '', + '结果及数据': r.data?.results_data || '', + '牛津评级': r.data?.oxford_level || '', + })); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, '提取结果'); + + // 添加任务信息Sheet + const taskInfo = [ + { 项目: '任务名称', 值: task.name }, + { 项目: '模板类型', 值: template.name }, + { 项目: '执行时间', 值: task.started_at }, + { 项目: '完成时间', 值: task.completed_at || '-' }, + { 项目: '总文献数', 值: task.total_documents }, + { 项目: '成功数', 值: task.completed_count - task.failed_count }, + { 项目: '失败数', 值: task.failed_count }, + { 项目: '总用时', 值: task.duration_seconds ? `${task.duration_seconds}秒` : '-' }, + ]; + const taskSheet = XLSX.utils.json_to_sheet(taskInfo); + XLSX.utils.book_append_sheet(workbook, taskSheet, '任务信息'); + + const filename = `批处理结果_${template.name}_${new Date().toISOString().split('T')[0]}.xlsx`; + XLSX.writeFile(workbook, filename); + } else { + // 自定义模板:3列表格 + const exportData = results + .filter(r => r.status === 'success') + .map(r => ({ + '序号': r.index, + '文献名称': r.document_name, + '提取结果': r.data?.extracted_text || '', + })); + + const worksheet = XLSX.utils.json_to_sheet(exportData); + const workbook = XLSX.utils.book_new(); + XLSX.utils.book_append_sheet(workbook, worksheet, '提取结果'); + + // 添加任务信息Sheet + const taskInfo = [ + { 项目: '任务名称', 值: task.name }, + { 项目: '模板类型', 值: '自定义模板' }, + { 项目: '执行时间', 值: task.started_at }, + { 项目: '完成时间', 值: task.completed_at || '-' }, + { 项目: '总文献数', 值: task.total_documents }, + { 项目: '成功数', 值: task.completed_count - task.failed_count }, + { 项目: '失败数', 值: task.failed_count }, + { 项目: '总用时', 值: task.duration_seconds ? `${task.duration_seconds}秒` : '-' }, + ]; + const taskSheet = XLSX.utils.json_to_sheet(taskInfo); + XLSX.utils.book_append_sheet(workbook, taskSheet, '任务信息'); + + const filename = `批处理结果_自定义任务_${new Date().toISOString().split('T')[0]}.xlsx`; + XLSX.writeFile(workbook, filename); + } + + message.success('Excel文件已导出'); + } catch (error: any) { + message.error(`导出失败: ${error.message}`); + } + }; + + return ( +
+ {/* 结果头部 */} +
+
+ 📊 + 批处理结果 +
+ + {task.failed_count > 0 && ( + + )} + + + +
+ + {/* 任务摘要 */} +
+
+ 任务名称: + {task.name} +
+
+ 模板类型: + + {template ? template.name : '自定义模板'} + +
+
+ 总文献数: + {task.total_documents} +
+
+ 成功: + + {task.completed_count - task.failed_count} + +
+
+ 失败: + + {task.failed_count} + +
+ {task.duration_seconds && ( +
+ 总用时: + + {Math.floor(task.duration_seconds / 60)}分{task.duration_seconds % 60}秒 + +
+ )} +
+ + {/* 结果表格 */} +
+ {template ? ( + + ) : ( + + )} +
+
+ ); +} + diff --git a/frontend/src/components/chat/CapacityIndicator.css b/frontend/src/components/chat/CapacityIndicator.css new file mode 100644 index 00000000..284b2269 --- /dev/null +++ b/frontend/src/components/chat/CapacityIndicator.css @@ -0,0 +1,87 @@ +/* Phase 2: 容量指示器样式 */ + +.capacity-indicator { + background: linear-gradient(135deg, #e6f7ff 0%, #f0f8ff 100%); + border: 1px solid #91d5ff; + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.capacity-title { + font-size: 14px; + font-weight: 600; + color: #262626; + margin: 0 0 16px 0; +} + +.capacity-item { + margin-bottom: 16px; +} + +.capacity-item:last-of-type { + margin-bottom: 0; +} + +.capacity-label { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 13px; + color: #595959; + margin-bottom: 6px; +} + +.capacity-value { + font-family: 'Monaco', 'Menlo', monospace; + font-weight: 600; + color: #262626; +} + +.capacity-bar-container { + height: 8px; + background: #fff; + border-radius: 4px; + overflow: hidden; + margin-bottom: 4px; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.capacity-bar { + height: 100%; + transition: all 0.3s ease; + border-radius: 4px; +} + +.capacity-percentage { + font-size: 12px; + color: #8c8c8c; + text-align: right; +} + +.capacity-warning { + margin-top: 12px; + padding: 8px 12px; + background: #fff7e6; + border: 1px solid #ffd591; + border-radius: 4px; + font-size: 12px; + color: #d46b08; + text-align: center; +} + +.capacity-info { + margin-top: 12px; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.8); + border-radius: 4px; + font-size: 12px; + color: #595959; + text-align: center; +} + + + + + + diff --git a/frontend/src/components/chat/CapacityIndicator.tsx b/frontend/src/components/chat/CapacityIndicator.tsx new file mode 100644 index 00000000..31244146 --- /dev/null +++ b/frontend/src/components/chat/CapacityIndicator.tsx @@ -0,0 +1,93 @@ +/** + * Phase 2: 容量指示器组件 + * 显示文件数和Token使用情况 + */ + +import './CapacityIndicator.css' + +interface CapacityIndicatorProps { + selectedFiles: number + totalFiles: number + usedTokens: number + maxTokens: number +} + +export function CapacityIndicator({ + selectedFiles, + totalFiles, + usedTokens, + maxTokens, +}: CapacityIndicatorProps) { + const filePercentage = Math.round((selectedFiles / totalFiles) * 100) + const tokenPercentage = Math.round((usedTokens / maxTokens) * 100) + + const getStatusColor = (percentage: number) => { + if (percentage > 90) return '#ff4d4f' // 红色 + if (percentage > 75) return '#faad14' // 橙色 + return '#52c41a' // 绿色 + } + + return ( +
+

📊 容量使用情况

+ + {/* 文件数 */} +
+
+ 文件数 + + {selectedFiles} / {totalFiles} 篇 + +
+
+
+
+
{filePercentage}%
+
+ + {/* Token数 */} +
+
+ Token容量 + + {Math.round(usedTokens / 1000)}K / {Math.round(maxTokens / 1000)}K + +
+
+
+
+
{tokenPercentage}%
+
+ + {/* 警告信息 */} + {(filePercentage > 90 || tokenPercentage > 90) && ( +
+ ⚠️ 容量即将达到上限,建议减少文献数量 +
+ )} + + {/* 剩余Token信息 */} +
+ 剩余对话空间: {Math.round((maxTokens - usedTokens) / 1000)}K tokens +
+
+ ) +} + + + + + + diff --git a/frontend/src/components/chat/CustomTable.tsx b/frontend/src/components/chat/CustomTable.tsx new file mode 100644 index 00000000..ee9be456 --- /dev/null +++ b/frontend/src/components/chat/CustomTable.tsx @@ -0,0 +1,142 @@ +/** + * Phase 3: 批处理模式 - 自定义模板结果表格(3列) + */ + +import React, { useState } from 'react'; +import { Table, Tag, Modal } from 'antd'; +import { CheckCircleOutlined, CloseCircleOutlined, EyeOutlined } from '@ant-design/icons'; +import type { BatchResult } from '../../api/batchApi'; + +interface CustomTableProps { + results: BatchResult[]; +} + +export function CustomTable({ results }: CustomTableProps) { + const [viewingResult, setViewingResult] = useState(null); + + const columns = [ + { + title: '#', + dataIndex: 'index', + key: 'index', + width: 60, + }, + { + title: '文献名称', + dataIndex: 'document_name', + key: 'document_name', + width: 250, + render: (text: string, record: BatchResult) => ( +
+
{text}
+
+ {record.status === 'failed' ? ( + }>失败 + ) : ( + }>成功 + )} + {record.processing_time_ms && ( + {Math.round(record.processing_time_ms / 1000)}秒 + )} +
+
+ ), + }, + { + title: '提取结果', + dataIndex: ['data', 'extracted_text'], + key: 'extracted_text', + ellipsis: true, + render: (text: string, record: BatchResult) => { + if (record.status === 'failed') { + return ( +
+ ❌ {record.error_message || '处理失败'} +
+ ); + } + + const preview = text ? text.substring(0, 200) : ''; + const hasMore = text && text.length > 200; + + return ( + + ); + }, + }, + ]; + + return ( + <> +
+ `共 ${total} 篇文献`, + }} + size="middle" + /> + + + {/* 查看完整内容的模态框 */} + setViewingResult(null)} + footer={null} + width={800} + className="result-detail-modal" + > + {viewingResult && ( +
+
+
提取结果:
+
+ {viewingResult.data?.extracted_text || '无内容'} +
+
+ + {viewingResult.processing_time_ms && ( +
+ + 处理时间: {Math.round(viewingResult.processing_time_ms / 1000)}秒 + + {viewingResult.tokens_used && ( + + Token使用: {viewingResult.tokens_used.toLocaleString()} + + )} +
+ )} +
+ )} +
+ + ); +} + + + + + + + + + diff --git a/frontend/src/components/chat/DeepReadMode.css b/frontend/src/components/chat/DeepReadMode.css new file mode 100644 index 00000000..285ddea5 --- /dev/null +++ b/frontend/src/components/chat/DeepReadMode.css @@ -0,0 +1,101 @@ +/* Phase 2: 逐篇精读模式样式 */ + +.deep-read-mode { + height: 100%; + display: flex; + flex-direction: column; + background: #fff; +} + +.deep-read-header { + padding: 20px; + background: #fff; + border-bottom: 1px solid #e8e8e8; +} + +.mode-info { + display: block; +} + +.mode-title { + font-size: 18px; + font-weight: 600; + color: #262626; + margin: 0 0 8px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.kb-name-badge { + font-size: 14px; + font-weight: 400; + color: #8c8c8c; +} + +.mode-desc { + font-size: 14px; + color: #595959; + margin: 0; +} + +.mode-desc strong { + color: #faad14; + font-weight: 600; +} + +.deep-read-chat { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.empty-state { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + background: #fafafa; +} + +.empty-icon { + font-size: 64px; + margin-bottom: 16px; +} + +.empty-title { + font-size: 18px; + font-weight: 600; + color: #262626; + margin-bottom: 8px; +} + +.empty-hint { + font-size: 14px; + color: #8c8c8c; + margin-bottom: 24px; + text-align: center; +} + +.empty-features { + display: flex; + gap: 24px; +} + +.feature-item { + font-size: 13px; + color: #52c41a; + display: flex; + align-items: center; + gap: 4px; +} + +.deep-read-input { + flex-shrink: 0; + border-top: 1px solid #e8e8e8; +} + diff --git a/frontend/src/components/chat/DeepReadMode.tsx b/frontend/src/components/chat/DeepReadMode.tsx new file mode 100644 index 00000000..071523f1 --- /dev/null +++ b/frontend/src/components/chat/DeepReadMode.tsx @@ -0,0 +1,103 @@ +/** + * Phase 2: 逐篇精读模式主组件 + */ + +import { DeepReadModeState } from '../../types/chat' +import { DocumentSwitcher } from './DocumentSwitcher' +import MessageList from './MessageList' +import MessageInput from './MessageInput' +import './DeepReadMode.css' + +interface DeepReadModeProps { + state: DeepReadModeState + kbName: string + onSwitch: (docId: string) => void + onSendMessage: (content: string) => void + loading: boolean + streamingContent?: string +} + +export function DeepReadMode({ + state, + kbName, + onSwitch, + onSendMessage, + loading, + streamingContent, +}: DeepReadModeProps) { + const { selectedDocs, currentDoc, currentConversation } = state + + // 转换消息格式 + const messages = currentConversation.map((msg: any) => ({ + id: msg.id, + conversationId: 'deep-read', + role: msg.role as 'user' | 'assistant', + content: msg.content, + createdAt: msg.timestamp.toISOString(), + })) + + return ( +
+ {/* 顶部信息栏 */} +
+
+

+ 🔍 逐篇精读模式 + - {kbName} +

+

+ 已选 {selectedDocs.length} 篇文献,可逐篇深度分析 +

+
+
+ + {/* 文献切换器 */} + + + {/* 聊天区域 */} +
+ {messages.length === 0 && !loading ? ( +
+
🔍
+
开始精读 {currentDoc?.filename}
+
+ 针对这篇文献提问,AI将提供详细深入的分析 +
+
+
+ ✓ 深度理解文献内容 +
+
+ ✓ 精确信息提取 +
+
+ ✓ 多轮推理对话 +
+
+
+ ) : ( + + )} +
+ + {/* 消息输入 */} +
+ onSendMessage(content)} + loading={loading} + knowledgeBases={[]} + placeholder={`关于 ${currentDoc?.filename || '当前文献'} 的问题...`} + /> +
+
+ ) +} + diff --git a/frontend/src/components/chat/DocumentSelection.css b/frontend/src/components/chat/DocumentSelection.css new file mode 100644 index 00000000..5170b771 --- /dev/null +++ b/frontend/src/components/chat/DocumentSelection.css @@ -0,0 +1,134 @@ +/* Phase 3: 批处理模式 - 文献选择样式 */ + +.document-selection { + padding: 20px; + background: #fff; + border-radius: 8px; + border: 1px solid #e8e8e8; + margin-top: 16px; + display: flex; + flex-direction: column; + height: 100%; +} + +.selection-summary { + padding: 12px; + background: #f5f5f5; + border-radius: 6px; + margin-bottom: 12px; + font-size: 14px; +} + +.summary-text { + color: #262626; +} + +.summary-text strong { + color: #1890ff; + font-size: 16px; +} + +.summary-warning { + color: #fa8c16; + margin-left: 8px; + font-size: 13px; +} + +.summary-error { + color: #ff4d4f; + margin-left: 8px; + font-size: 13px; + font-weight: 500; +} + +.selection-actions { + margin-bottom: 12px; +} + +.selection-search { + margin-bottom: 12px; +} + +.document-list { + flex: 1; + overflow-y: auto; + border: 1px solid #d9d9d9; + border-radius: 6px; + padding: 8px; + min-height: 200px; + max-height: 400px; +} + +.document-item { + display: flex; + align-items: center; + padding: 10px 12px; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s; + margin-bottom: 4px; +} + +.document-item:hover { + background: #f5f5f5; +} + +.document-item.selected { + background: #e6f7ff; + border: 1px solid #91d5ff; +} + +.document-item .ant-checkbox { + margin-right: 12px; +} + +.document-info { + flex: 1; + display: flex; + align-items: center; + justify-content: space-between; +} + +.document-name { + font-size: 14px; + color: #262626; + flex: 1; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.document-tokens { + font-size: 12px; + color: #8c8c8c; + margin-left: 12px; + white-space: nowrap; +} + +.empty-list { + display: flex; + align-items: center; + justify-content: center; + height: 150px; + color: #bfbfbf; + font-size: 14px; +} + +.selection-hint { + margin-top: 12px; + padding: 8px 12px; + background: #f0f7ff; + border-left: 3px solid #1890ff; + border-radius: 4px; + font-size: 13px; + color: #595959; +} + + + + + + + + + diff --git a/frontend/src/components/chat/DocumentSelection.tsx b/frontend/src/components/chat/DocumentSelection.tsx new file mode 100644 index 00000000..91fd5cae --- /dev/null +++ b/frontend/src/components/chat/DocumentSelection.tsx @@ -0,0 +1,156 @@ +/** + * Phase 3: 批处理模式 - 文献选择组件 + */ + +import { useState, useMemo } from 'react'; +import { Checkbox, Input, Button, Space } from 'antd'; +import { SearchOutlined } from '@ant-design/icons'; +import './DocumentSelection.css'; + +interface Document { + id: string; + filename: string; + tokensCount?: number; +} + +interface DocumentSelectionProps { + documents: Document[]; + selectedIds: string[]; + onSelectionChange: (selectedIds: string[]) => void; +} + +export function DocumentSelection({ + documents = [], // 默认空数组 + selectedIds = [], // 默认空数组 + onSelectionChange, +}: DocumentSelectionProps) { + const [searchText, setSearchText] = useState(''); + + // 过滤文档 + const filteredDocuments = useMemo(() => { + if (!searchText.trim()) return documents; + + const search = searchText.toLowerCase(); + return documents.filter(doc => + doc.filename.toLowerCase().includes(search) + ); + }, [documents, searchText]); + + // 全选/清空/反选 + const handleSelectAll = () => { + onSelectionChange(filteredDocuments.map(doc => doc.id)); + }; + + const handleClearAll = () => { + onSelectionChange([]); + }; + + const handleInvert = () => { + const allIds = new Set(documents.map(doc => doc.id)); + const selectedSet = new Set(selectedIds); + const inverted = Array.from(allIds).filter(id => !selectedSet.has(id)); + onSelectionChange(inverted); + }; + + // 切换单个文档 + const handleToggleDocument = (docId: string) => { + if (selectedIds.includes(docId)) { + onSelectionChange(selectedIds.filter(id => id !== docId)); + } else { + onSelectionChange([...selectedIds, docId]); + } + }; + + // 格式化Token数 + const formatTokens = (tokens?: number) => { + if (!tokens) return ''; + if (tokens > 1000) { + return `${Math.round(tokens / 1000)}K`; + } + return tokens.toString(); + }; + + return ( +
+
第2步:选择文献
+ + {/* 统计信息 */} +
+ + 已选择:{selectedIds.length} / 50 篇 + + {selectedIds.length < 3 && ( + (至少需要3篇) + )} + {selectedIds.length > 50 && ( + (最多50篇) + )} +
+ + {/* 操作按钮 */} +
+ + + + + +
+ + {/* 搜索框 */} +
+ } + value={searchText} + onChange={(e) => setSearchText(e.target.value)} + allowClear + /> +
+ + {/* 文档列表 */} +
+ {filteredDocuments.length === 0 ? ( +
+ {searchText ? '没有匹配的文献' : '知识库中没有文献'} +
+ ) : ( + filteredDocuments.map(doc => ( +
handleToggleDocument(doc.id)} + > + handleToggleDocument(doc.id)} + onClick={(e) => e.stopPropagation()} + /> +
+
{doc.filename}
+ {doc.tokensCount && ( +
+ {formatTokens(doc.tokensCount)} tokens +
+ )} +
+
+ )) + )} +
+ + {/* 提示信息 */} + {selectedIds.length > 0 && ( +
+ 💡 已选择 {selectedIds.length} 篇文献,预计处理时间约 {Math.round(selectedIds.length * 20 / 60)} 分钟 +
+ )} +
+ ); +} + diff --git a/frontend/src/components/chat/DocumentSelector.css b/frontend/src/components/chat/DocumentSelector.css new file mode 100644 index 00000000..45a0fecc --- /dev/null +++ b/frontend/src/components/chat/DocumentSelector.css @@ -0,0 +1,202 @@ +/* Phase 2: 文献选择器样式 */ + +.doc-selector-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.doc-selector-modal { + background: #fff; + border-radius: 8px; + width: 90%; + max-width: 800px; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.doc-selector-header { + padding: 20px 24px; + border-bottom: 1px solid #e8e8e8; + display: flex; + justify-content: space-between; + align-items: center; +} + +.doc-selector-title { + font-size: 18px; + font-weight: 600; + color: #262626; + margin: 0; +} + +.doc-selector-close { + width: 32px; + height: 32px; + border: none; + background: #f5f5f5; + border-radius: 4px; + font-size: 24px; + color: #8c8c8c; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s; +} + +.doc-selector-close:hover { + background: #e8e8e8; + color: #262626; +} + +.doc-selector-list { + flex: 1; + overflow-y: auto; + padding: 16px 24px; +} + +.doc-selector-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 6px; + margin-bottom: 12px; + cursor: pointer; + transition: all 0.2s; +} + +.doc-selector-item:hover:not(.disabled) { + border-color: #1890ff; + background: #f0f8ff; +} + +.doc-selector-item.selected { + border-color: #1890ff; + background: #e6f7ff; +} + +.doc-selector-item.disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.doc-checkbox { + width: 18px; + height: 18px; + cursor: pointer; + flex-shrink: 0; +} + +.doc-info { + flex: 1; + min-width: 0; +} + +.doc-filename { + font-size: 14px; + font-weight: 500; + color: #262626; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.doc-meta { + font-size: 12px; + color: #8c8c8c; +} + +.doc-selector-footer { + padding: 16px 24px; + border-top: 1px solid #e8e8e8; + background: #fafafa; +} + +.doc-selector-stats { + display: flex; + justify-content: space-between; + margin-bottom: 16px; + padding: 12px; + background: #fff; + border-radius: 4px; + border: 1px solid #e8e8e8; +} + +.stat-item { + display: flex; + align-items: center; + gap: 8px; +} + +.stat-label { + font-size: 13px; + color: #595959; +} + +.stat-value { + font-size: 14px; + font-weight: 600; + color: #262626; + font-family: 'Monaco', 'Menlo', monospace; +} + +.doc-selector-actions { + display: flex; + gap: 12px; +} + +.btn-cancel, +.btn-confirm { + flex: 1; + height: 40px; + border-radius: 4px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + border: none; +} + +.btn-cancel { + background: #fff; + border: 1px solid #d9d9d9; + color: #262626; +} + +.btn-cancel:hover { + border-color: #40a9ff; + color: #40a9ff; +} + +.btn-confirm { + background: #1890ff; + color: #fff; +} + +.btn-confirm:hover:not(.disabled) { + background: #40a9ff; +} + +.btn-confirm.disabled { + background: #d9d9d9; + cursor: not-allowed; +} + + + + + + diff --git a/frontend/src/components/chat/DocumentSelector.tsx b/frontend/src/components/chat/DocumentSelector.tsx new file mode 100644 index 00000000..13c4b7ae --- /dev/null +++ b/frontend/src/components/chat/DocumentSelector.tsx @@ -0,0 +1,129 @@ +/** + * Phase 2: 文献选择器(用于逐篇精读模式) + */ + +import { useState } from 'react' +import { Document } from '../../types/index' +import './DocumentSelector.css' + +interface DocumentSelectorProps { + documents: Document[] + maxSelection: number + onConfirm: (selected: Document[]) => void + onCancel: () => void +} + +export function DocumentSelector({ + documents, + maxSelection, + onConfirm, + onCancel, +}: DocumentSelectorProps) { + const [selected, setSelected] = useState>(new Set()) + + const toggleSelect = (docId: string) => { + setSelected(prev => { + const newSet = new Set(prev) + if (newSet.has(docId)) { + newSet.delete(docId) + } else { + if (newSet.size < maxSelection) { + newSet.add(docId) + } + } + return newSet + }) + } + + const selectedDocs = documents.filter(doc => selected.has(doc.id)) + const totalTokens = selectedDocs.reduce((sum, doc) => sum + (doc.tokensCount || 0), 0) + + const handleConfirm = () => { + if (selectedDocs.length > 0) { + onConfirm(selectedDocs) + } + } + + return ( +
+
e.stopPropagation()}> +
+

+ 📄 选择要精读的文献(最多{maxSelection}篇) +

+ +
+ + {/* 文献列表 */} +
+ {documents.map(doc => { + const isSelected = selected.has(doc.id) + const isDisabled = !isSelected && selected.size >= maxSelection + + return ( +
!isDisabled && toggleSelect(doc.id)} + > + +
+
{doc.filename}
+
+ {((doc.fileSizeBytes || 0) / 1024).toFixed(1)} KB •{' '} + {doc.tokensCount ? `${Math.round(doc.tokensCount / 1000)}K tokens` : '未知大小'} +
+
+
+ ) + })} +
+ + {/* 底部统计 */} +
+
+
+ 已选文献: + + {selected.size} / {maxSelection} 篇 + +
+
+ Token总计: + {totalTokens.toLocaleString()} +
+
+ + {/* 操作按钮 */} +
+ + +
+
+
+
+ ) +} + + + + + + diff --git a/frontend/src/components/chat/DocumentSwitcher.css b/frontend/src/components/chat/DocumentSwitcher.css new file mode 100644 index 00000000..0386adc5 --- /dev/null +++ b/frontend/src/components/chat/DocumentSwitcher.css @@ -0,0 +1,101 @@ +/* Phase 2: 文献切换器样式 */ + +.doc-switcher { + padding: 16px 20px; + background: linear-gradient(135deg, #fff7e6 0%, #fffbe6 100%); + border-bottom: 1px solid #ffd591; +} + +.doc-switcher-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 12px; +} + +.current-doc-label { + font-size: 13px; + color: #8c8c8c; + font-weight: 500; +} + +.current-doc-name { + font-size: 14px; + color: #262626; + font-weight: 600; + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.doc-switcher-list { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.doc-switch-btn { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background: #fff; + cursor: pointer; + font-size: 13px; + color: #595959; + transition: all 0.2s; + max-width: 200px; +} + +.doc-switch-btn:hover { + border-color: #faad14; + background: #fffbe6; +} + +.doc-switch-btn.active { + border-color: #faad14; + background: #fff7e6; + color: #d46b08; + font-weight: 500; +} + +.doc-indicator { + font-size: 12px; + flex-shrink: 0; +} + +.doc-switch-btn.active .doc-indicator { + color: #faad14; +} + +.doc-btn-name { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.doc-btn-tokens { + flex-shrink: 0; + font-family: 'Monaco', 'Menlo', monospace; + font-size: 11px; + color: #52c41a; + background: #f6ffed; + padding: 2px 4px; + border-radius: 2px; +} + +.doc-switch-btn.active .doc-btn-tokens { + background: #fff7e6; + color: #faad14; +} + + + + + + diff --git a/frontend/src/components/chat/DocumentSwitcher.tsx b/frontend/src/components/chat/DocumentSwitcher.tsx new file mode 100644 index 00000000..04765231 --- /dev/null +++ b/frontend/src/components/chat/DocumentSwitcher.tsx @@ -0,0 +1,60 @@ +/** + * Phase 2: 文献切换器(用于逐篇精读模式) + */ + +import { Document } from '../../types/index' +import './DocumentSwitcher.css' + +interface DocumentSwitcherProps { + currentDoc: Document | undefined + selectedDocs: Document[] + onSwitch: (docId: string) => void +} + +export function DocumentSwitcher({ + currentDoc, + selectedDocs, + onSwitch, +}: DocumentSwitcherProps) { + if (!currentDoc || selectedDocs.length === 0) { + return null + } + + return ( +
+
+ 当前文献: + {currentDoc.filename} +
+ + {selectedDocs.length > 1 && ( +
+ {selectedDocs.map(doc => ( + + ))} +
+ )} +
+ ) +} + + + + + + diff --git a/frontend/src/components/chat/FullTextMode.css b/frontend/src/components/chat/FullTextMode.css new file mode 100644 index 00000000..597dddde --- /dev/null +++ b/frontend/src/components/chat/FullTextMode.css @@ -0,0 +1,94 @@ +/* Phase 2: 全文阅读模式样式 */ + +.fulltext-mode { + height: 100%; + display: flex; + flex-direction: column; + background: #fff; +} + +.fulltext-chat { + flex: 1; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.fulltext-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + background: #fafafa; +} + +.empty-icon { + font-size: 64px; + margin-bottom: 16px; +} + +.empty-title { + font-size: 20px; + font-weight: 600; + color: #262626; + margin-bottom: 12px; +} + +.empty-desc { + font-size: 14px; + color: #8c8c8c; + margin-bottom: 12px; + text-align: center; +} + +.empty-desc strong { + color: #1890ff; + font-weight: 600; +} + +.empty-hint { + font-size: 13px; + color: #faad14; + margin-bottom: 32px; + text-align: center; + font-weight: 500; +} + +.example-queries { + max-width: 500px; + width: 100%; +} + +.example-title { + font-size: 13px; + font-weight: 600; + color: #595959; + margin-bottom: 12px; +} + +.example-item { + padding: 12px 16px; + background: #fff; + border: 1px solid #e8e8e8; + border-radius: 6px; + margin-bottom: 8px; + font-size: 13px; + color: #595959; + transition: all 0.2s; + cursor: pointer; +} + +.example-item:hover { + border-color: #1890ff; + background: #f0f8ff; + color: #1890ff; +} + +.fulltext-input { + flex-shrink: 0; + border-top: 1px solid #e8e8e8; +} + diff --git a/frontend/src/components/chat/FullTextMode.tsx b/frontend/src/components/chat/FullTextMode.tsx new file mode 100644 index 00000000..3fc9d657 --- /dev/null +++ b/frontend/src/components/chat/FullTextMode.tsx @@ -0,0 +1,73 @@ +/** + * Phase 2: 全文阅读模式主组件(优化版 - 移除顶部Header) + */ + +import { FullTextModeState } from '../../types/chat' +import MessageList from './MessageList' +import MessageInput from './MessageInput' +import './FullTextMode.css' + +interface FullTextModeProps { + state: FullTextModeState + onSendMessage: (content: string) => void + messages: any[] + loading: boolean + streamingContent?: string +} + +export function FullTextMode({ + state, + onSendMessage, + messages, + loading, + streamingContent, +}: FullTextModeProps) { + return ( +
+ {/* 聊天区域 */} +
+ {messages.length === 0 && !loading ? ( +
+
🌍
+
全文阅读模式已就绪
+
+ 已加载 {state.selectedFiles} 篇文献,可进行跨文献综合分析 +
+
+ 💡 点击右上角"用量说明"按钮查看详细信息 +
+
+
示例问题:
+
+ 💡 这些文献的主要研究方向是什么? +
+
+ 📊 总结这些研究的主要发现和结论 +
+
+ 🔍 这些文献中有哪些研究方法? +
+
+
+ ) : ( + + )} +
+ + {/* 消息输入 */} +
+ onSendMessage(content)} + loading={loading} + knowledgeBases={[]} + placeholder={`基于${state.selectedFiles}篇文献提问...`} + /> +
+
+ ) +} + diff --git a/frontend/src/components/chat/FullTextModeHeader.css b/frontend/src/components/chat/FullTextModeHeader.css new file mode 100644 index 00000000..a5e399eb --- /dev/null +++ b/frontend/src/components/chat/FullTextModeHeader.css @@ -0,0 +1,107 @@ +/* Phase 2: 全文阅读模式头部样式 */ + +.fulltext-header { + padding: 20px; + background: #fff; + border-bottom: 1px solid #e8e8e8; + flex-shrink: 0; + max-height: 40vh; + overflow-y: auto; +} + +.fulltext-info { + margin-bottom: 20px; +} + +.fulltext-title { + font-size: 18px; + font-weight: 600; + color: #262626; + margin: 0 0 8px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.kb-name { + font-size: 14px; + font-weight: 400; + color: #8c8c8c; +} + +.fulltext-desc { + font-size: 14px; + color: #595959; + margin: 0 0 4px 0; +} + +.fulltext-desc strong { + color: #1890ff; + font-weight: 600; +} + +.fulltext-reason { + font-size: 13px; + color: #8c8c8c; + margin: 0; +} + +.loaded-docs { + margin-top: 16px; +} + +.loaded-docs-title { + font-size: 14px; + font-weight: 600; + color: #262626; + margin: 0 0 12px 0; +} + +.doc-list { + max-height: 200px; + overflow-y: auto; + border: 1px solid #e8e8e8; + border-radius: 4px; + background: #fafafa; +} + +.doc-item { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border-bottom: 1px solid #e8e8e8; + font-size: 13px; +} + +.doc-item:last-child { + border-bottom: none; +} + +.doc-item:hover { + background: #f0f8ff; +} + +.doc-icon { + flex-shrink: 0; + font-size: 16px; +} + +.doc-name { + flex: 1; + color: #262626; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.doc-tokens { + flex-shrink: 0; + font-family: 'Monaco', 'Menlo', monospace; + font-size: 12px; + color: #52c41a; + background: #f6ffed; + padding: 2px 6px; + border-radius: 3px; +} + diff --git a/frontend/src/components/chat/FullTextModeHeader.tsx b/frontend/src/components/chat/FullTextModeHeader.tsx new file mode 100644 index 00000000..c9a4630c --- /dev/null +++ b/frontend/src/components/chat/FullTextModeHeader.tsx @@ -0,0 +1,73 @@ +/** + * Phase 2: 全文阅读模式头部组件 + */ + +import { FullTextModeState } from '../../types/chat' +import { CapacityIndicator } from './CapacityIndicator' +import './FullTextModeHeader.css' + +interface FullTextModeHeaderProps { + state: FullTextModeState + kbName: string +} + +export function FullTextModeHeader({ state, kbName }: FullTextModeHeaderProps) { + const getReasonText = (reason: string) => { + switch (reason) { + case 'all_included': + return '✅ 所有文献已加载' + case 'file_limit': + return '⚠️ 已达到文件数限制(50篇)' + case 'token_limit': + return '⚠️ 已达到Token限制(980K)' + default: + return '' + } + } + + return ( +
+
+

+ 🌍 全文阅读模式 + - {kbName} +

+

+ 已加载 {state.selectedFiles} 篇文献,可进行跨文献综合分析 +

+

{getReasonText(state.reason)}

+
+ + + + {/* 已加载文档列表 */} +
+

已加载文献

+
+ {state.loadedDocs.map(doc => ( +
+ 📄 + {doc.filename} + {doc.tokensCount && ( + + {Math.round(doc.tokensCount / 1000)}K + + )} +
+ ))} +
+
+
+ ) +} + + + + + + diff --git a/frontend/src/components/chat/MessageList.tsx b/frontend/src/components/chat/MessageList.tsx index d2577569..2ca0da79 100644 --- a/frontend/src/components/chat/MessageList.tsx +++ b/frontend/src/components/chat/MessageList.tsx @@ -5,6 +5,7 @@ import ReactMarkdown from 'react-markdown'; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneLight } from 'react-syntax-highlighter/dist/esm/styles/prism'; import remarkGfm from 'remark-gfm'; +import rehypeRaw from 'rehype-raw'; import './MessageList.css'; const { Text } = Typography; @@ -41,6 +42,74 @@ const MessageList: React.FC = ({ scrollToBottom(); }, [messages, streamingContent]); + // 处理引用标记的点击事件(事件委托) + useEffect(() => { + const handleCitationClick = (e: MouseEvent) => { + const target = e.target as HTMLElement; + // 检查target是否有classList属性(防止非Element节点) + if (target.classList && target.classList.contains('citation-badge')) { + const citationNum = target.getAttribute('data-citation-num'); + if (citationNum) { + const targetElement = document.getElementById(`citation-detail-${citationNum}`); + if (targetElement) { + targetElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + // 高亮闪烁效果 + targetElement.style.backgroundColor = '#fffbe6'; + setTimeout(() => { + targetElement.style.backgroundColor = ''; + }, 2000); + } + } + } + }; + + // 添加鼠标悬停效果 + const handleCitationMouseEnter = (e: MouseEvent) => { + const target = e.target as HTMLElement; + // 检查target是否有classList属性(防止非Element节点) + if (target.classList && target.classList.contains('citation-badge')) { + target.style.backgroundColor = '#bae7ff'; + target.style.borderColor = '#40a9ff'; + } + }; + + const handleCitationMouseLeave = (e: MouseEvent) => { + const target = e.target as HTMLElement; + // 检查target是否有classList属性(防止非Element节点) + if (target.classList && target.classList.contains('citation-badge')) { + target.style.backgroundColor = '#e6f7ff'; + target.style.borderColor = '#91d5ff'; + } + }; + + document.addEventListener('click', handleCitationClick); + document.addEventListener('mouseenter', handleCitationMouseEnter, true); + document.addEventListener('mouseleave', handleCitationMouseLeave, true); + + return () => { + document.removeEventListener('click', handleCitationClick); + document.removeEventListener('mouseenter', handleCitationMouseEnter, true); + document.removeEventListener('mouseleave', handleCitationMouseLeave, true); + }; + }, []); + + // 将文本中的引用标记转换为HTML + const convertCitationsToHTML = (text: string): string => { + // 匹配 [来源数字] 模式 + const citationRegex = /\[来源(\d+)\]/g; + + return text.replace(citationRegex, (match, num) => { + // 生成唯一的ID用于跳转 + const citationId = `citation-ref-${num}-${Math.random().toString(36).substr(2, 9)}`; + + // 返回HTML字符串(单行,避免换行导致的解析问题) + return `${match}`; + }); + }; + // Markdown代码块渲染 const MarkdownComponents = { code({ node, inline, className, children, ...props }: any) { @@ -107,9 +176,10 @@ const MessageList: React.FC = ({
- {message.content} + {convertCitationsToHTML(message.content)}
)} @@ -155,9 +225,10 @@ const MessageList: React.FC = ({
- {streamingContent} + {convertCitationsToHTML(streamingContent)}
diff --git a/frontend/src/components/chat/ModeSelector.css b/frontend/src/components/chat/ModeSelector.css new file mode 100644 index 00000000..799c5ebe --- /dev/null +++ b/frontend/src/components/chat/ModeSelector.css @@ -0,0 +1,104 @@ +/* Phase 2: 模式选择器样式 */ + +.mode-selector { + width: 280px; + height: 100%; + background: #fafafa; + border-right: 1px solid #e8e8e8; + display: flex; + flex-direction: column; + overflow-y: auto; +} + +.mode-section { + padding: 16px; + border-bottom: 1px solid #e8e8e8; +} + +.mode-section-title { + font-size: 14px; + font-weight: 600; + color: #262626; + margin: 0 0 12px 0; +} + +.mode-button { + width: 100%; + padding: 12px; + margin-bottom: 8px; + border: 1px solid #d9d9d9; + border-radius: 6px; + background: #fff; + cursor: pointer; + display: flex; + align-items: center; + gap: 10px; + font-size: 14px; + color: #262626; + transition: all 0.2s; +} + +.mode-button:hover:not(.disabled) { + border-color: #1890ff; + background: #f0f8ff; +} + +.mode-button.active { + border-color: #1890ff; + background: #e6f7ff; + color: #1890ff; + font-weight: 500; +} + +.mode-button.disabled { + opacity: 0.5; + cursor: not-allowed; + background: #f5f5f5; +} + +.mode-icon { + font-size: 20px; + flex-shrink: 0; +} + +.mode-button-content { + flex: 1; + text-align: left; +} + +.mode-hint { + font-size: 12px; + color: #8c8c8c; + margin-top: 4px; +} + +.mode-button.active .mode-hint { + color: #69b1ff; +} + +.kb-select { + width: 100%; + padding: 8px 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + font-size: 14px; + background: #fff; + cursor: pointer; + transition: all 0.2s; +} + +.kb-select:hover { + border-color: #40a9ff; +} + +.kb-select:focus { + outline: none; + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + + + + + + diff --git a/frontend/src/components/chat/ModeSelector.tsx b/frontend/src/components/chat/ModeSelector.tsx new file mode 100644 index 00000000..3e52d11c --- /dev/null +++ b/frontend/src/components/chat/ModeSelector.tsx @@ -0,0 +1,114 @@ +/** + * Phase 2: 聊天模式选择器(侧边栏) + */ + +import { useEffect } from 'react' +import { ChatPageState, ChatBaseMode, KnowledgeBaseMode } from '../../types/chat' +import { useKnowledgeBaseStore } from '../../stores/useKnowledgeBaseStore' +import './ModeSelector.css' + +interface ModeSelectorProps { + state: ChatPageState + onModeChange: (mode: ChatBaseMode) => void + onKbSelect: (kbId: string) => void + onKbModeChange: (mode: KnowledgeBaseMode) => void +} + +export function ModeSelector({ + state, + onModeChange, + onKbSelect, + onKbModeChange, +}: ModeSelectorProps) { + const { knowledgeBases, fetchKnowledgeBases } = useKnowledgeBaseStore() + + useEffect(() => { + fetchKnowledgeBases() + }, [fetchKnowledgeBases]) + + return ( +
+ {/* 基础模式选择 */} +
+

对话模式

+ + + + +
+ + {/* 知识库模式选项 */} + {state.baseMode === 'knowledge_base' && ( + <> +
+

知识库选择

+ +
+ + {state.selectedKbId && ( +
+

工作模式

+ + + + + + +
+ )} + + )} +
+ ) +} + diff --git a/frontend/src/components/chat/ModelSelector.tsx b/frontend/src/components/chat/ModelSelector.tsx index a06148da..991bd5ef 100644 --- a/frontend/src/components/chat/ModelSelector.tsx +++ b/frontend/src/components/chat/ModelSelector.tsx @@ -5,7 +5,7 @@ import './ModelSelector.css'; const { Option } = Select; -export type ModelType = 'deepseek-v3' | 'qwen3-72b' | 'gemini-pro'; +export type ModelType = 'deepseek-v3' | 'qwen3-72b' | 'qwen-long' | 'gemini-pro'; interface ModelInfo { value: ModelType; @@ -21,19 +21,28 @@ const models: ModelInfo[] = [ { value: 'deepseek-v3', label: 'DeepSeek-V3', - description: '高性价比,推理能力强', + description: '高性价比,推理能力强(128K上下文)', icon: , color: '#1890ff', - features: ['快速响应', '成本优化', '长文本处理'], + features: ['快速响应', '成本优化', '128K上下文'], recommended: true, }, { value: 'qwen3-72b', label: 'Qwen3-72B', - description: '阿里通义千问,中文理解优秀', + description: '阿里通义千问,中文理解优秀(32K上下文)', icon: , color: '#52c41a', - features: ['中文优化', '多轮对话', '专业领域'], + features: ['中文优化', '多轮对话', '32K上下文'], + }, + { + value: 'qwen-long', + label: 'Qwen-Long', + description: '超长上下文模型,1M tokens(约700页)', + icon: , + color: '#722ed1', + features: ['1M超长上下文', '全文档理解', '文献综述'], + recommended: false, }, { value: 'gemini-pro', diff --git a/frontend/src/components/chat/PresetTable.tsx b/frontend/src/components/chat/PresetTable.tsx new file mode 100644 index 00000000..aa828237 --- /dev/null +++ b/frontend/src/components/chat/PresetTable.tsx @@ -0,0 +1,150 @@ +/** + * Phase 3: 批处理模式 - 预设模板结果表格(8列) + */ + +import React from 'react'; +import { Table, Tag, Tooltip } from 'antd'; +import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'; +import type { BatchResult } from '../../api/batchApi'; + +interface PresetTableProps { + results: BatchResult[]; +} + +export function PresetTable({ results }: PresetTableProps) { + const columns = [ + { + title: '#', + dataIndex: 'index', + key: 'index', + width: 60, + align: 'center' as const, + }, + { + title: '文献名称', + dataIndex: 'document_name', + key: 'document_name', + width: 120, + render: (text: string, record: BatchResult) => ( +
+ +
+ {text} +
+
+ {record.status === 'failed' ? ( + } style={{ fontSize: '11px' }}>失败 + ) : ( + } style={{ fontSize: '11px' }}>成功 + )} +
+ ), + }, + { + title: '研究目的', + dataIndex: ['data', 'research_purpose'], + key: 'research_purpose', + width: 150, + render: (text: string) => ( +
{text || '-'}
+ ), + }, + { + title: '研究设计', + dataIndex: ['data', 'research_design'], + key: 'research_design', + width: 100, + render: (text: string) => ( +
{text || '-'}
+ ), + }, + { + title: '研究对象', + dataIndex: ['data', 'research_subjects'], + key: 'research_subjects', + width: 200, + render: (text: string) => ( +
{text || '-'}
+ ), + }, + { + title: '样本量', + dataIndex: ['data', 'sample_size'], + key: 'sample_size', + width: 100, + render: (text: string) => ( +
{text || '-'}
+ ), + }, + { + title: '干预组', + dataIndex: ['data', 'intervention_group'], + key: 'intervention_group', + width: 150, + render: (text: string) => ( +
{text || '-'}
+ ), + }, + { + title: '对照组', + dataIndex: ['data', 'control_group'], + key: 'control_group', + width: 150, + render: (text: string) => ( +
{text || '-'}
+ ), + }, + { + title: '结果及数据', + dataIndex: ['data', 'results_data'], + key: 'results_data', + width: 300, + render: (text: string) => ( +
{text || '-'}
+ ), + }, + { + title: '牛津评级', + dataIndex: ['data', 'oxford_level'], + key: 'oxford_level', + width: 100, + align: 'center' as const, + fixed: 'right' as const, // 固定在右侧 + render: (text: string) => { + const getLevelColor = (level: string) => { + if (level.startsWith('1')) return 'green'; + if (level.startsWith('2')) return 'blue'; + if (level.startsWith('3')) return 'orange'; + return 'default'; + }; + + return text ? ( + {text} + ) : ( + '-' + ); + }, + }, + ]; + + return ( +
+
`共 ${total} 篇文献`, + position: ['bottomLeft'], // 分页器放在左边 + }} + scroll={{ x: 1400 }} + size="middle" + bordered + /> + + ); +} + diff --git a/frontend/src/components/chat/TaskDefinition.css b/frontend/src/components/chat/TaskDefinition.css new file mode 100644 index 00000000..ba8b71ac --- /dev/null +++ b/frontend/src/components/chat/TaskDefinition.css @@ -0,0 +1,150 @@ +/* Phase 3: 批处理模式 - 任务定义样式 */ + +.task-definition { + padding: 20px; + background: #fff; + border-radius: 8px; + border: 1px solid #e8e8e8; +} + +.task-section-title { + font-size: 16px; + font-weight: 600; + color: #262626; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 2px solid #1890ff; +} + +.task-template-selector { + margin-bottom: 20px; +} + +.template-label { + font-size: 14px; + font-weight: 500; + color: #595959; + margin-bottom: 12px; +} + +.task-template-selector .ant-radio-group { + display: flex; + flex-direction: column; + gap: 12px; +} + +.task-template-selector .ant-radio-wrapper { + display: flex; + align-items: flex-start; + padding: 12px; + border: 1px solid #d9d9d9; + border-radius: 6px; + transition: all 0.3s; + margin: 0; +} + +.task-template-selector .ant-radio-wrapper:hover { + border-color: #1890ff; + background: #f0f7ff; +} + +.task-template-selector .ant-radio-wrapper-checked { + border-color: #1890ff; + background: #e6f7ff; +} + +.template-name { + font-weight: 500; + color: #262626; + margin-right: 8px; +} + +.template-desc { + font-size: 13px; + color: #8c8c8c; +} + +/* 字段预览 */ +.template-fields-preview { + background: #fafafa; + border: 1px solid #e8e8e8; + border-radius: 6px; + padding: 16px; + margin-top: 16px; +} + +.preview-title { + font-size: 14px; + font-weight: 500; + color: #595959; + margin-bottom: 12px; +} + +.fields-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.field-item { + display: flex; + align-items: baseline; + font-size: 13px; + color: #595959; + padding: 4px 0; +} + +.field-number { + color: #1890ff; + font-weight: 500; + margin-right: 4px; + min-width: 20px; +} + +.field-label { + font-weight: 500; + color: #262626; + margin-right: 4px; +} + +.field-desc { + color: #8c8c8c; + font-size: 12px; +} + +/* 自定义提示词 */ +.custom-prompt-input { + margin-top: 16px; +} + +.input-label { + font-size: 14px; + font-weight: 500; + color: #595959; + margin-bottom: 8px; +} + +.prompt-textarea { + font-family: 'Consolas', 'Monaco', monospace; + font-size: 13px; + line-height: 1.6; +} + +.prompt-hint { + margin-top: 8px; + font-size: 12px; + color: #8c8c8c; + padding: 8px 12px; + background: #fffbe6; + border-left: 3px solid #faad14; + border-radius: 4px; +} + + + + + + + + + diff --git a/frontend/src/components/chat/TaskDefinition.tsx b/frontend/src/components/chat/TaskDefinition.tsx new file mode 100644 index 00000000..ea78a5eb --- /dev/null +++ b/frontend/src/components/chat/TaskDefinition.tsx @@ -0,0 +1,98 @@ +/** + * Phase 3: 批处理模式 - 任务定义组件 + */ + +import { Radio, Input } from 'antd'; +import type { BatchTemplate } from '../../api/batchApi'; +import './TaskDefinition.css'; + +const { TextArea } = Input; + +interface TaskDefinitionProps { + templates: BatchTemplate[]; + selectedTemplate: BatchTemplate | null; + customPrompt: string; + onSelectTemplate: (template: BatchTemplate | null) => void; + onCustomPromptChange: (prompt: string) => void; +} + +export function TaskDefinition({ + templates = [], // 默认空数组 + selectedTemplate, + customPrompt, + onSelectTemplate, + onCustomPromptChange, +}: TaskDefinitionProps) { + const isCustom = !selectedTemplate; + + return ( +
+
第1步:定义任务
+ +
+
任务模板:
+ + { + const value = e.target.value; + if (value === 'custom') { + onSelectTemplate(null); + } else { + const template = templates.find(t => t.id === value); + onSelectTemplate(template || null); + } + }} + > + {templates.map(template => ( + + {template.name} + {template.description} + + ))} + + + 自定义任务 + 自定义提示词,结果以文本块显示 + + +
+ + {/* 预设模板的字段预览 */} + {selectedTemplate && ( +
+
提取字段({selectedTemplate.output_fields.length}个):
+
+ {selectedTemplate.output_fields.map((field, index) => ( +
+ {index + 1}. + {field.label} + {field.description && ( + - {field.description} + )} +
+ ))} +
+
+ )} + + {/* 自定义提示词输入 */} + {isCustom && ( +
+
自定义提示词:
+