update:1.更新根据分析建议重新生成章节内容

This commit is contained in:
xiamuceer
2025-11-11 19:50:12 +08:00
parent 5b46d657f3
commit 913edd0cce
30 changed files with 3896 additions and 1928 deletions
+156
View File
@@ -0,0 +1,156 @@
# 安装和测试指南
## 1. 安装前端依赖
在完成所有代码更改后,需要安装新添加的npm包:
```bash
cd frontend
npm install
```
这将安装以下新依赖:
- `react-diff-viewer-continued`: 用于版本对比的diff查看器
## 2. 重启后端服务
由于修改了数据模型,需要重启后端服务以加载新的模型定义:
```bash
# 在项目根目录
cd backend
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
```
## 3. 启动前端开发服务器
```bash
cd frontend
npm run dev
```
## 4. 测试功能流程
### 4.1 基本流程测试
1. **打开章节列表**
- 进入任意项目
- 查看章节列表
2. **分析章节**
- 点击某个章节的"分析"按钮
- 等待AI分析完成
- 查看分析结果和改进建议
3. **重新生成章节**
- 在分析结果页面,点击"根据建议重新生成"
- 选择要应用的建议
- 可以添加自定义修改要求
- 配置生成参数(字数、保留元素等)
- 勾选"保存为版本历史"(不勾选自动应用)
- 点击"开始重新生成"
- 观察流式生成过程
4. **查看版本对比**
- 生成完成后,点击"查看版本对比"按钮
- 进入版本管理器界面
5. **版本管理操作**
- **版本列表**: 查看所有历史版本
- **预览版本**: 点击"预览"查看某个版本的完整内容
- **对比版本**:
- 点击第一个版本的"对比"按钮
- 再点击第二个版本的"对比"按钮
- 自动切换到"对比"标签页
- 查看并排diff对比
- **恢复版本**: 点击"恢复"将章节内容还原到该版本
- **删除版本**: 删除不需要的历史版本(当前激活版本不能删除)
### 4.2 测试场景
#### 场景1:优化情感描写
1. 分析一个章节
2. 查看建议中关于情感的建议
3. 重新生成时选择情感相关建议
4. 设置重点优化方向为"情感渲染"
5. 生成后对比新旧版本的差异
#### 场景2:调整节奏
1. 选择节奏问题的建议
2. 添加自定义指令:"加快前半部分节奏,增强紧张感"
3. 设置目标字数适当减少(如从3000减到2500)
4. 生成后查看结构变化
#### 场景3:版本管理
1. 对同一章节重新生成多次(使用不同建议)
2. 在版本管理器中浏览所有版本
3. 对比不同版本的差异
4. 选择最满意的版本恢复
## 5. 验证清单
- [ ] 依赖安装无错误
- [ ] 前后端服务正常启动
- [ ] 章节分析功能正常
- [ ] 重新生成功能正常
- [ ] 流式生成显示正常
- [ ] 版本保存成功
- [ ] 版本列表显示正确
- [ ] 版本预览功能正常
- [ ] 版本对比diff显示正确
- [ ] 版本恢复功能正常
- [ ] 版本删除功能正常
- [ ] 移动端适配正常
## 6. 常见问题
### Q1: 依赖安装失败
```bash
# 清除缓存重试
npm cache clean --force
npm install
```
### Q2: 后端启动报错
- 检查是否运行了数据库迁移脚本
- 确认模型定义与数据库表结构一致
### Q3: 版本对比不显示
- 检查浏览器控制台是否有JavaScript错误
- 确认react-diff-viewer-continued正确安装
### Q4: 重新生成后看不到新内容
- 检查是否勾选了"自动应用"
- 查看版本管理器中是否有新版本记录
## 7. 性能优化建议
1. **首次加载优化**
- react-diff-viewer-continued是较大的依赖
- 可以考虑代码分割(lazy loading
2. **版本列表优化**
- 如果版本过多,考虑分页加载
- 添加版本数量限制提示
3. **diff计算优化**
- 对于超长文本,可以限制diff行数
- 添加加载提示
## 8. 下一步优化方向
1. **AI质量评分对比**
- 在版本对比时显示质量分数变化
- 自动标注改进/退步的指标
2. **批量操作**
- 支持批量删除历史版本
- 支持版本导出/导入
3. **协作功能**
- 版本评论和讨论
- 多人协作编辑
4. **智能推荐**
- 基于历史生成结果推荐最佳配置
- 学习用户偏好自动调整参数
+337 -23
View File
@@ -14,6 +14,7 @@
"dayjs": "^1.11.13",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"zustand": "^5.0.8"
@@ -135,7 +136,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
@@ -191,7 +191,6 @@
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.3",
@@ -225,7 +224,6 @@
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -235,7 +233,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
@@ -277,7 +274,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -287,7 +283,6 @@
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -321,7 +316,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.4"
@@ -378,7 +372,6 @@
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -393,7 +386,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
@@ -412,7 +404,6 @@
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
@@ -422,18 +413,136 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/babel-plugin": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.16.7",
"@babel/runtime": "^7.18.3",
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/serialize": "^1.3.3",
"babel-plugin-macros": "^3.1.0",
"convert-source-map": "^1.5.0",
"escape-string-regexp": "^4.0.0",
"find-root": "^1.1.0",
"source-map": "^0.5.7",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/babel-plugin/node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
"license": "MIT"
},
"node_modules/@emotion/babel-plugin/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/cache": {
"version": "11.14.0",
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.9.0",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2",
"@emotion/weak-memoize": "^0.4.0",
"stylis": "4.2.0"
}
},
"node_modules/@emotion/cache/node_modules/stylis": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
"node_modules/@emotion/css": {
"version": "11.13.5",
"resolved": "https://registry.npmjs.org/@emotion/css/-/css-11.13.5.tgz",
"integrity": "sha512-wQdD0Xhkn3Qy2VNcIzbLP9MR8TafI0MJb7BEAXKp+w4+XqErksWR4OXomuDzPsN4InLdGhVe6EYcn2ZIUCpB8w==",
"license": "MIT",
"dependencies": {
"@emotion/babel-plugin": "^11.13.5",
"@emotion/cache": "^11.13.5",
"@emotion/serialize": "^1.3.3",
"@emotion/sheet": "^1.4.0",
"@emotion/utils": "^1.4.2"
}
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@emotion/memoize": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==",
"license": "MIT"
},
"node_modules/@emotion/serialize": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "^0.9.2",
"@emotion/memoize": "^0.9.0",
"@emotion/unitless": "^0.10.0",
"@emotion/utils": "^1.4.2",
"csstype": "^3.0.2"
}
},
"node_modules/@emotion/serialize/node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/serialize/node_modules/@emotion/unitless": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==",
"license": "MIT"
},
"node_modules/@emotion/sheet": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
"license": "MIT"
},
"node_modules/@emotion/utils": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==",
"license": "MIT"
},
"node_modules/@emotion/weak-memoize": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
@@ -1089,7 +1198,6 @@
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
@@ -1111,7 +1219,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
@@ -1121,14 +1228,12 @@
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
@@ -1726,6 +1831,12 @@
"undici-types": "~7.16.0"
}
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
"node_modules/@types/prop-types": {
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
@@ -2211,6 +2322,21 @@
"proxy-from-env": "^1.1.0"
}
},
"node_modules/babel-plugin-macros": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz",
"integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5",
"cosmiconfig": "^7.0.0",
"resolve": "^1.19.0"
},
"engines": {
"node": ">=10",
"npm": ">=6"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2303,7 +2429,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -2414,6 +2539,31 @@
"toggle-selection": "^1.0.6"
}
},
"node_modules/cosmiconfig": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
"integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
"license": "MIT",
"dependencies": {
"@types/parse-json": "^4.0.0",
"import-fresh": "^3.2.1",
"parse-json": "^5.0.0",
"path-type": "^4.0.0",
"yaml": "^1.10.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2454,7 +2604,6 @@
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -2484,6 +2633,15 @@
"node": ">=0.4.0"
}
},
"node_modules/diff": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz",
"integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.3.1"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2505,6 +2663,15 @@
"dev": true,
"license": "ISC"
},
"node_modules/error-ex": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.2.1"
}
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2606,7 +2773,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -2879,6 +3045,12 @@
"node": ">=8"
}
},
"node_modules/find-root": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==",
"license": "MIT"
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3147,7 +3319,6 @@
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
@@ -3170,6 +3341,27 @@
"node": ">=0.8.19"
}
},
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
"license": "MIT"
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3233,7 +3425,6 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
@@ -3249,6 +3440,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -3309,6 +3506,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
"license": "MIT"
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
@@ -3431,7 +3634,6 @@
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
@@ -3530,7 +3732,6 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
@@ -3539,6 +3740,24 @@
"node": ">=6"
}
},
"node_modules/parse-json": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
"json-parse-even-better-errors": "^2.3.0",
"lines-and-columns": "^1.1.6"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -3559,11 +3778,25 @@
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"license": "MIT"
},
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
@@ -4316,6 +4549,32 @@
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-diff-viewer-continued": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/react-diff-viewer-continued/-/react-diff-viewer-continued-3.4.0.tgz",
"integrity": "sha512-kMZmUyb3Pv5L9vUtCfIGYsdOHs8mUojblGy1U1Sm0D7FhAOEsH9QhnngEIRo5hXWIPNGupNRJls1TJ6Eqx84eg==",
"license": "MIT",
"dependencies": {
"@emotion/css": "^11.11.2",
"classnames": "^2.3.2",
"diff": "^5.1.0",
"memoize-one": "^6.0.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 8"
},
"peerDependencies": {
"react": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-diff-viewer-continued/node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -4423,11 +4682,30 @@
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
@@ -4561,6 +4839,15 @@
"node": ">=8"
}
},
"node_modules/source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4609,6 +4896,18 @@
"node": ">=8"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
@@ -4951,6 +5250,21 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+1
View File
@@ -16,6 +16,7 @@
"dayjs": "^1.11.13",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
"zustand": "^5.0.8"
+98 -1
View File
@@ -10,9 +10,12 @@ import {
CheckCircleOutlined,
ClockCircleOutlined,
CloseCircleOutlined,
ReloadOutlined
ReloadOutlined,
EditOutlined
} from '@ant-design/icons';
import type { AnalysisTask, ChapterAnalysisResponse } from '../types';
import ChapterRegenerationModal from './ChapterRegenerationModal';
import ChapterContentComparison from './ChapterContentComparison';
// 判断是否为移动设备
const isMobileDevice = () => window.innerWidth < 768;
@@ -29,6 +32,11 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isMobile, setIsMobile] = useState(isMobileDevice());
const [regenerationModalVisible, setRegenerationModalVisible] = useState(false);
const [comparisonModalVisible, setComparisonModalVisible] = useState(false);
const [chapterInfo, setChapterInfo] = useState<{ title: string; chapter_number: number; content: string } | null>(null);
const [newGeneratedContent, setNewGeneratedContent] = useState('');
const [newContentWordCount, setNewContentWordCount] = useState(0);
useEffect(() => {
if (visible && chapterId) {
@@ -54,6 +62,17 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
setLoading(true);
setError(null);
// 同时获取章节信息
const chapterResponse = await fetch(`/api/chapters/${chapterId}`);
if (chapterResponse.ok) {
const chapterData = await chapterResponse.json();
setChapterInfo({
title: chapterData.title,
chapter_number: chapterData.chapter_number,
content: chapterData.content || ''
});
}
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
if (response.status === 404) {
@@ -199,6 +218,17 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
);
};
// 将分析建议转换为重新生成组件需要的格式
const convertSuggestionsForRegeneration = () => {
if (!analysis?.analysis?.suggestions) return [];
return analysis.analysis.suggestions.map((suggestion, index) => ({
category: '改进建议',
content: suggestion,
priority: index < 3 ? 'high' : 'medium'
}));
};
const renderAnalysisResult = () => {
if (!analysis) return null;
@@ -215,6 +245,29 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
icon: <TrophyOutlined />,
children: (
<div style={{ height: isMobile ? 'calc(80vh - 180px)' : 'calc(90vh - 220px)', overflowY: 'auto', paddingRight: '8px' }}>
{/* 根据建议重新生成按钮 */}
{analysis_data.suggestions && analysis_data.suggestions.length > 0 && (
<Alert
message="发现改进建议"
description={
<div>
<p style={{ marginBottom: 12 }}>AI已分析出 {analysis_data.suggestions.length} </p>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => setRegenerationModalVisible(true)}
size={isMobile ? 'small' : 'middle'}
>
</Button>
</div>
}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
)}
<Card title="整体评分" style={{ marginBottom: 16 }} size={isMobile ? 'small' : 'default'}>
<Row gutter={isMobile ? 8 : 16}>
<Col span={isMobile ? 12 : 6}>
@@ -560,6 +613,50 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
{task && task.status !== 'completed' && renderProgress()}
{task && task.status === 'completed' && analysis && renderAnalysisResult()}
{/* 重新生成Modal */}
{chapterInfo && (
<ChapterRegenerationModal
visible={regenerationModalVisible}
onCancel={() => setRegenerationModalVisible(false)}
onSuccess={(newContent: string, wordCount: number) => {
// 保存新生成的内容
setNewGeneratedContent(newContent);
setNewContentWordCount(wordCount);
// 关闭重新生成对话框
setRegenerationModalVisible(false);
// 打开对比界面
setComparisonModalVisible(true);
}}
chapterId={chapterId}
chapterTitle={chapterInfo.title}
chapterNumber={chapterInfo.chapter_number}
suggestions={convertSuggestionsForRegeneration()}
hasAnalysis={true}
/>
)}
{/* 内容对比组件 */}
{chapterInfo && comparisonModalVisible && (
<ChapterContentComparison
visible={comparisonModalVisible}
onClose={() => setComparisonModalVisible(false)}
chapterId={chapterId}
chapterTitle={chapterInfo.title}
originalContent={chapterInfo.content}
newContent={newGeneratedContent}
wordCount={newContentWordCount}
onApply={() => {
// 应用新内容后刷新章节信息
fetchAnalysisStatus();
}}
onDiscard={() => {
// 放弃新内容,清空状态
setNewGeneratedContent('');
setNewContentWordCount(0);
}}
/>
)}
</Modal>
);
}
@@ -0,0 +1,218 @@
import React, { useState } from 'react';
import { Modal, Button, Card, Statistic, Row, Col, message } from 'antd';
import { CheckOutlined, CloseOutlined, SwapOutlined } from '@ant-design/icons';
import ReactDiffViewer from 'react-diff-viewer-continued';
interface ChapterContentComparisonProps {
visible: boolean;
onClose: () => void;
chapterId: string;
chapterTitle: string;
originalContent: string;
newContent: string;
wordCount: number;
onApply: () => void;
onDiscard: () => void;
}
const ChapterContentComparison: React.FC<ChapterContentComparisonProps> = ({
visible,
onClose,
chapterId,
chapterTitle,
originalContent,
newContent,
wordCount,
onApply,
onDiscard
}) => {
const [applying, setApplying] = useState(false);
const [viewMode, setViewMode] = useState<'split' | 'unified'>('split');
const originalWordCount = originalContent.length;
const wordCountDiff = wordCount - originalWordCount;
const wordCountDiffPercent = ((wordCountDiff / originalWordCount) * 100).toFixed(1);
const handleApply = async () => {
setApplying(true);
try {
const response = await fetch(`/api/chapters/${chapterId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: newContent
})
});
if (!response.ok) {
throw new Error('应用新内容失败');
}
message.success('新内容已应用!正在触发章节分析...');
// 触发章节分析
try {
const analysisResponse = await fetch(`/api/chapters/${chapterId}/analyze`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
if (analysisResponse.ok) {
message.success('章节分析已开始,请稍后查看结果');
} else {
message.warning('章节分析触发失败,您可以手动触发分析');
}
} catch (analysisError) {
console.error('触发分析失败:', analysisError);
message.warning('章节分析触发失败,您可以手动触发分析');
}
onApply();
onClose();
} catch (error: any) {
message.error(error.message || '应用失败');
} finally {
setApplying(false);
}
};
const handleDiscard = () => {
Modal.confirm({
title: '确认放弃',
content: '确定要放弃新生成的内容吗?此操作不可恢复。',
okText: '确定放弃',
cancelText: '取消',
okButtonProps: { danger: true },
onOk: () => {
onDiscard();
onClose();
message.info('已放弃新内容');
}
});
};
return (
<Modal
title={`内容对比 - ${chapterTitle}`}
open={visible}
onCancel={onClose}
width="95%"
centered
style={{ maxWidth: 1600 }}
footer={[
<Button
key="discard"
danger
icon={<CloseOutlined />}
onClick={handleDiscard}
>
</Button>,
<Button
key="toggle"
icon={<SwapOutlined />}
onClick={() => setViewMode(viewMode === 'split' ? 'unified' : 'split')}
>
</Button>,
<Button
key="apply"
type="primary"
icon={<CheckOutlined />}
loading={applying}
onClick={handleApply}
>
</Button>
]}
>
{/* 统计信息 */}
<Card size="small" style={{ marginBottom: 16 }}>
<Row gutter={16}>
<Col span={6}>
<Statistic
title="原内容字数"
value={originalWordCount}
suffix="字"
/>
</Col>
<Col span={6}>
<Statistic
title="新内容字数"
value={wordCount}
suffix="字"
/>
</Col>
<Col span={6}>
<Statistic
title="字数变化"
value={wordCountDiff}
suffix="字"
valueStyle={{ color: wordCountDiff > 0 ? '#3f8600' : '#cf1322' }}
prefix={wordCountDiff > 0 ? '+' : ''}
/>
</Col>
<Col span={6}>
<Statistic
title="变化比例"
value={wordCountDiffPercent}
suffix="%"
valueStyle={{ color: Math.abs(parseFloat(wordCountDiffPercent)) < 10 ? '#1890ff' : '#faad14' }}
prefix={wordCountDiff > 0 ? '+' : ''}
/>
</Col>
</Row>
</Card>
{/* 内容对比 */}
<div style={{
maxHeight: 'calc(90vh - 300px)',
overflow: 'auto',
border: '1px solid #d9d9d9',
borderRadius: 4
}}>
<ReactDiffViewer
oldValue={originalContent}
newValue={newContent}
splitView={viewMode === 'split'}
leftTitle="原内容"
rightTitle="新内容"
showDiffOnly={false}
useDarkTheme={false}
styles={{
variables: {
light: {
diffViewerBackground: '#fff',
addedBackground: '#e6ffed',
addedColor: '#24292e',
removedBackground: '#ffeef0',
removedColor: '#24292e',
wordAddedBackground: '#acf2bd',
wordRemovedBackground: '#fdb8c0',
addedGutterBackground: '#cdffd8',
removedGutterBackground: '#ffdce0',
gutterBackground: '#f6f8fa',
gutterBackgroundDark: '#f3f4f6',
highlightBackground: '#fffbdd',
highlightGutterBackground: '#fff5b1',
},
},
line: {
padding: '10px 2px',
fontSize: '14px',
lineHeight: '20px',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}
}}
/>
</div>
</Modal>
);
};
export default ChapterContentComparison;
@@ -0,0 +1,402 @@
import React, { useState, useEffect } from 'react';
import {
Modal,
Form,
Input,
Button,
Checkbox,
InputNumber,
Space,
Alert,
Divider,
Progress,
Tag,
message,
Collapse,
Card,
Radio
} from 'antd';
import {
ReloadOutlined,
CheckCircleOutlined,
CloseCircleOutlined
} from '@ant-design/icons';
import { ssePost } from '../utils/sseClient';
const { TextArea } = Input;
const { Panel } = Collapse;
interface Suggestion {
category: string;
content: string;
priority: string;
}
interface ChapterRegenerationModalProps {
visible: boolean;
onCancel: () => void;
onSuccess: (newContent: string, wordCount: number) => void;
chapterId: string;
chapterTitle: string;
chapterNumber: number;
suggestions?: Suggestion[];
hasAnalysis: boolean;
}
const ChapterRegenerationModal: React.FC<ChapterRegenerationModalProps> = ({
visible,
onCancel,
onSuccess,
chapterId,
chapterTitle,
chapterNumber,
suggestions = [],
hasAnalysis
}) => {
const [form] = Form.useForm();
const [loading, setLoading] = useState(false);
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<'idle' | 'generating' | 'success' | 'error'>('idle');
const [errorMessage, setErrorMessage] = useState('');
const [wordCount, setWordCount] = useState(0);
const [selectedSuggestions, setSelectedSuggestions] = useState<number[]>([]);
const [modificationSource, setModificationSource] = useState<'custom' | 'analysis_suggestions' | 'mixed'>('custom');
useEffect(() => {
if (visible) {
// 重置状态
setStatus('idle');
setProgress(0);
setErrorMessage('');
setWordCount(0);
setSelectedSuggestions([]);
// 如果有分析建议,默认选择混合模式
if (hasAnalysis && suggestions.length > 0) {
setModificationSource('mixed');
} else {
setModificationSource('custom');
}
// 设置默认值
form.setFieldsValue({
modification_source: hasAnalysis && suggestions.length > 0 ? 'mixed' : 'custom',
target_word_count: 3000,
preserve_structure: false,
preserve_character_traits: true,
focus_areas: []
});
}
}, [visible, hasAnalysis, suggestions.length, form]);
const handleSubmit = async () => {
try {
const values = await form.validateFields();
// 验证至少提供一种修改指令
if (values.modification_source === 'custom' && !values.custom_instructions?.trim()) {
message.error('请输入自定义修改要求');
return;
}
if (values.modification_source === 'analysis_suggestions' && selectedSuggestions.length === 0) {
message.error('请选择至少一条分析建议');
return;
}
if (values.modification_source === 'mixed' &&
selectedSuggestions.length === 0 &&
!values.custom_instructions?.trim()) {
message.error('请至少选择一条建议或输入自定义要求');
return;
}
setLoading(true);
setStatus('generating');
setProgress(0);
setWordCount(0);
// 构建请求数据
const requestData: any = {
modification_source: values.modification_source,
custom_instructions: values.custom_instructions,
selected_suggestion_indices: selectedSuggestions,
preserve_elements: {
preserve_structure: values.preserve_structure,
preserve_dialogues: values.preserve_dialogues || [],
preserve_plot_points: values.preserve_plot_points || [],
preserve_character_traits: values.preserve_character_traits
},
style_id: values.style_id,
target_word_count: values.target_word_count,
focus_areas: values.focus_areas || []
};
let accumulatedContent = '';
let currentWordCount = 0;
// 使用SSE流式生成
await ssePost(
`/api/chapters/${chapterId}/regenerate-stream`,
requestData,
{
onProgress: (_msg: string, prog: number, _status: string, wordCount?: number) => {
// 后端发送的进度消息
setProgress(prog);
// 如果后端提供了word_count,使用它;否则使用累积的字数
if (wordCount !== undefined) {
setWordCount(wordCount);
currentWordCount = wordCount;
}
},
onChunk: (content: string) => {
// 累积内容块
accumulatedContent += content;
// 仅作为备用字数统计
currentWordCount = accumulatedContent.length;
// 不再自己计算进度,完全依赖后端发送的progress消息
},
onResult: (data: any) => {
// 生成完成,确保使用最新的累积内容
setProgress(100);
setStatus('success');
const finalWordCount = data.word_count || currentWordCount;
setWordCount(finalWordCount);
message.success('重新生成完成!');
// 直接调用onSuccess打开对比界面,传递最终的累积内容
setTimeout(() => {
onSuccess(accumulatedContent, finalWordCount);
}, 500);
},
onComplete: () => {
// SSE完成
},
onError: (error: string, code?: number) => {
console.error('SSE Error:', error, code);
setStatus('error');
setErrorMessage(error || '生成失败');
message.error('重新生成失败: ' + (error || '未知错误'));
}
}
);
} catch (error: any) {
console.error('提交失败:', error);
setStatus('error');
setErrorMessage(error.message || '提交失败');
message.error('操作失败: ' + (error.message || '未知错误'));
} finally {
setLoading(false);
}
};
const handleSuggestionSelect = (index: number, checked: boolean) => {
if (checked) {
setSelectedSuggestions([...selectedSuggestions, index]);
} else {
setSelectedSuggestions(selectedSuggestions.filter(i => i !== index));
}
};
const handleCancel = () => {
if (loading) {
Modal.confirm({
title: '确认取消',
content: '生成正在进行中,确定要取消吗?',
onOk: () => {
setLoading(false);
setStatus('idle');
onCancel();
}
});
} else {
onCancel();
}
};
return (
<Modal
title={`重新生成章节 - 第${chapterNumber}章:${chapterTitle}`}
open={visible}
onCancel={handleCancel}
width={800}
centered
footer={
status === 'success' ? null : (
[
<Button key="cancel" onClick={handleCancel} disabled={loading}>
</Button>,
<Button
key="submit"
type="primary"
onClick={handleSubmit}
loading={loading}
icon={<ReloadOutlined />}
>
</Button>
]
)
}
>
{status === 'generating' && (
<Alert
message="正在重新生成中..."
description={
<div>
<Progress percent={progress} status="active" />
<div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
{wordCount}
</div>
</div>
}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
)}
{status === 'success' && (
<Alert
message="重新生成成功!"
description={`共生成 ${wordCount}`}
type="success"
showIcon
icon={<CheckCircleOutlined />}
style={{ marginBottom: 16 }}
/>
)}
{status === 'error' && (
<Alert
message="生成失败"
description={errorMessage}
type="error"
showIcon
icon={<CloseCircleOutlined />}
style={{ marginBottom: 16 }}
/>
)}
<Form
form={form}
layout="vertical"
disabled={loading || status === 'success'}
>
{/* 修改来源 */}
<Form.Item
name="modification_source"
label="修改来源"
rules={[{ required: true, message: '请选择修改来源' }]}
>
<Radio.Group onChange={(e) => setModificationSource(e.target.value)}>
<Radio value="custom"></Radio>
{hasAnalysis && suggestions.length > 0 && (
<>
<Radio value="analysis_suggestions"></Radio>
<Radio value="mixed"></Radio>
</>
)}
</Radio.Group>
</Form.Item>
{/* 分析建议选择 */}
{hasAnalysis && suggestions.length > 0 &&
(modificationSource === 'analysis_suggestions' || modificationSource === 'mixed') && (
<Form.Item label={`选择分析建议 (${selectedSuggestions.length}/${suggestions.length})`}>
<Card size="small" style={{ maxHeight: 300, overflow: 'auto' }}>
<Space direction="vertical" style={{ width: '100%' }}>
{suggestions.map((suggestion, index) => (
<Checkbox
key={index}
checked={selectedSuggestions.includes(index)}
onChange={(e) => handleSuggestionSelect(index, e.target.checked)}
>
<Space>
<Tag color={
suggestion.priority === 'high' ? 'red' :
suggestion.priority === 'medium' ? 'orange' : 'blue'
}>
{suggestion.category}
</Tag>
<span style={{ fontSize: 13 }}>{suggestion.content}</span>
</Space>
</Checkbox>
))}
</Space>
</Card>
</Form.Item>
)}
{/* 自定义修改要求 */}
{(modificationSource === 'custom' || modificationSource === 'mixed') && (
<Form.Item
name="custom_instructions"
label="自定义修改要求"
tooltip="描述你希望如何改进这个章节"
>
<TextArea
rows={4}
placeholder="例如:增强情感渲染,让主角的内心戏更加细腻..."
showCount
maxLength={1000}
/>
</Form.Item>
)}
{/* 高级选项 */}
<Collapse ghost>
<Panel header="高级选项" key="advanced">
{/* 重点优化方向 */}
<Form.Item
name="focus_areas"
label="重点优化方向"
>
<Checkbox.Group>
<Space direction="vertical">
<Checkbox value="pacing"></Checkbox>
<Checkbox value="emotion"></Checkbox>
<Checkbox value="description"></Checkbox>
<Checkbox value="dialogue"></Checkbox>
<Checkbox value="conflict"></Checkbox>
</Space>
</Checkbox.Group>
</Form.Item>
<Divider />
{/* 保留元素 */}
<Form.Item label="保留元素">
<Space direction="vertical" style={{ width: '100%' }}>
<Form.Item name="preserve_structure" valuePropName="checked" noStyle>
<Checkbox></Checkbox>
</Form.Item>
<Form.Item name="preserve_character_traits" valuePropName="checked" noStyle>
<Checkbox></Checkbox>
</Form.Item>
</Space>
</Form.Item>
<Divider />
{/* 生成参数 */}
<Form.Item
name="target_word_count"
label="目标字数"
tooltip="生成内容的目标字数,实际字数可能有±20%的浮动"
>
<InputNumber min={500} max={10000} step={500} style={{ width: '100%' }} />
</Form.Item>
</Panel>
</Collapse>
</Form>
</Modal>
);
};
export default ChapterRegenerationModal;
+100 -2
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Dropdown, Avatar, Space, Typography, message, Modal, Table, Button, Tag, Popconfirm, Pagination } from 'antd';
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined } from '@ant-design/icons';
import { Dropdown, Avatar, Space, Typography, message, Modal, Table, Button, Tag, Popconfirm, Pagination, Form, Input } from 'antd';
import { UserOutlined, LogoutOutlined, TeamOutlined, CrownOutlined, LockOutlined } from '@ant-design/icons';
import { authApi, userApi } from '../services/api';
import type { User } from '../types';
import type { MenuProps } from 'antd';
@@ -10,10 +10,13 @@ const { Text } = Typography;
export default function UserMenu() {
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [showUserManagement, setShowUserManagement] = useState(false);
const [showChangePassword, setShowChangePassword] = useState(false);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [changePasswordForm] = Form.useForm();
const [changingPassword, setChangingPassword] = useState(false);
useEffect(() => {
loadCurrentUser();
@@ -84,6 +87,21 @@ export default function UserMenu() {
}
};
const handleChangePassword = async (values: { oldPassword: string; newPassword: string }) => {
try {
setChangingPassword(true);
await authApi.setPassword(values.newPassword);
message.success('密码修改成功');
setShowChangePassword(false);
changePasswordForm.resetFields();
} catch (error: any) {
console.error('修改密码失败:', error);
message.error(error.response?.data?.detail || '修改密码失败');
} finally {
setChangingPassword(false);
}
};
const menuItems: MenuProps['items'] = [
{
key: 'user-info',
@@ -110,6 +128,15 @@ export default function UserMenu() {
}, {
type: 'divider' as const,
}] : []),
{
key: 'change-password',
icon: <LockOutlined />,
label: '修改密码',
onClick: () => setShowChangePassword(true),
},
{
type: 'divider',
},
{
key: 'logout',
icon: <LogoutOutlined />,
@@ -341,6 +368,77 @@ export default function UserMenu() {
</div>
</div>
</Modal>
<Modal
title="修改密码"
open={showChangePassword}
onCancel={() => {
setShowChangePassword(false);
changePasswordForm.resetFields();
}}
footer={null}
width={480}
centered
>
<Form
form={changePasswordForm}
layout="vertical"
onFinish={handleChangePassword}
autoComplete="off"
>
<Form.Item
label="新密码"
name="newPassword"
rules={[
{ required: true, message: '请输入新密码' },
{ min: 6, message: '密码至少6个字符' },
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请输入新密码(至少6个字符)"
autoComplete="new-password"
/>
</Form.Item>
<Form.Item
label="确认密码"
name="confirmPassword"
dependencies={['newPassword']}
rules={[
{ required: true, message: '请确认新密码' },
({ getFieldValue }) => ({
validator(_, value) {
if (!value || getFieldValue('newPassword') === value) {
return Promise.resolve();
}
return Promise.reject(new Error('两次输入的密码不一致'));
},
}),
]}
>
<Input.Password
prefix={<LockOutlined />}
placeholder="请再次输入新密码"
autoComplete="new-password"
/>
</Form.Item>
<Form.Item style={{ marginBottom: 0 }}>
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
<Button onClick={() => {
setShowChangePassword(false);
changePasswordForm.resetFields();
}}>
</Button>
<Button type="primary" htmlType="submit" loading={changingPassword}>
</Button>
</Space>
</Form.Item>
</Form>
</Modal>
</>
);
}
+140 -2
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Spin, Result, Button } from 'antd';
import { Spin, Result, Button, Modal, Input, message } from 'antd';
import { authApi } from '../services/api';
import AnnouncementModal from '../components/AnnouncementModal';
@@ -9,6 +9,11 @@ export default function AuthCallback() {
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
const [errorMessage, setErrorMessage] = useState('');
const [showAnnouncement, setShowAnnouncement] = useState(false);
const [showPasswordModal, setShowPasswordModal] = useState(false);
const [passwordStatus, setPasswordStatus] = useState<any>(null);
const [newPassword, setNewPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [settingPassword, setSettingPassword] = useState(false);
useEffect(() => {
const handleCallback = async () => {
@@ -17,8 +22,21 @@ export default function AuthCallback() {
// 这里只需要验证登录状态
await authApi.getCurrentUser();
// 检查密码状态
const pwdStatus = await authApi.getPasswordStatus();
setPasswordStatus(pwdStatus);
setStatus('success');
// 只有在用户完全没有密码时才显示密码设置提示
// 如果已经有密码(无论是默认密码还是自定义密码),都不再提示
if (!pwdStatus.has_password) {
setTimeout(() => {
setShowPasswordModal(true);
}, 1000);
return;
}
// 从 sessionStorage 获取重定向地址
const redirect = sessionStorage.getItem('login_redirect') || '/';
sessionStorage.removeItem('login_redirect');
@@ -105,6 +123,70 @@ export default function AuthCallback() {
localStorage.setItem('announcement_do_not_show_until', tomorrow.getTime().toString());
};
const handleSetPassword = async () => {
if (!newPassword) {
message.error('请输入新密码');
return;
}
if (newPassword.length < 6) {
message.error('密码长度至少为6个字符');
return;
}
if (newPassword !== confirmPassword) {
message.error('两次输入的密码不一致');
return;
}
setSettingPassword(true);
try {
await authApi.setPassword(newPassword);
message.success('密码设置成功');
setShowPasswordModal(false);
// 继续后续流程
const redirect = sessionStorage.getItem('login_redirect') || '/';
sessionStorage.removeItem('login_redirect');
const doNotShowUntil = localStorage.getItem('announcement_do_not_show_until');
const now = new Date().getTime();
if (!doNotShowUntil || now > parseInt(doNotShowUntil)) {
setTimeout(() => {
setShowAnnouncement(true);
}, 500);
} else {
setTimeout(() => {
navigate(redirect);
}, 500);
}
} catch (error) {
message.error('密码设置失败,请重试');
} finally {
setSettingPassword(false);
}
};
const handleSkipPasswordSetting = () => {
setShowPasswordModal(false);
// 继续后续流程
const redirect = sessionStorage.getItem('login_redirect') || '/';
sessionStorage.removeItem('login_redirect');
const doNotShowUntil = localStorage.getItem('announcement_do_not_show_until');
const now = new Date().getTime();
if (!doNotShowUntil || now > parseInt(doNotShowUntil)) {
setTimeout(() => {
setShowAnnouncement(true);
}, 500);
} else {
setTimeout(() => {
navigate(redirect);
}, 500);
}
};
return (
<>
<AnnouncementModal
@@ -112,6 +194,62 @@ export default function AuthCallback() {
onClose={handleAnnouncementClose}
onDoNotShowToday={handleDoNotShowToday}
/>
<Modal
title="设置账号密码"
open={showPasswordModal}
centered
onOk={handleSetPassword}
onCancel={handleSkipPasswordSetting}
confirmLoading={settingPassword}
okText="设置密码"
cancelText="暂不设置"
width={500}
>
<div style={{ marginBottom: 20 }}>
<p> Linux DO </p>
<p>使</p>
{passwordStatus?.default_password && (
<div style={{
background: '#f0f2f5',
padding: 12,
borderRadius: 4,
marginTop: 12
}}>
<strong></strong>{passwordStatus.username}<br/>
<strong></strong><code style={{
background: '#fff',
padding: '2px 8px',
borderRadius: 3,
color: '#1890ff',
fontSize: 14
}}>{passwordStatus.default_password}</code>
</div>
)}
</div>
<div style={{ marginTop: 20 }}>
<div style={{ marginBottom: 12 }}>
<label>6</label>
<Input.Password
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="请输入新密码"
style={{ marginTop: 4 }}
/>
</div>
<div>
<label></label>
<Input.Password
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="请再次输入密码"
style={{ marginTop: 4 }}
/>
</div>
</div>
</Modal>
<div style={{
display: 'flex',
justifyContent: 'center',
@@ -122,7 +260,7 @@ export default function AuthCallback() {
<Result
status="success"
title="登录成功"
subTitle={showAnnouncement ? "欢迎使用..." : "正在跳转..."}
subTitle={showPasswordModal ? "请设置账号密码..." : (showAnnouncement ? "欢迎使用..." : "正在跳转...")}
style={{ background: 'white', padding: 40, borderRadius: 8 }}
/>
</div>
+1
View File
@@ -600,6 +600,7 @@ export default function MCPPluginsPage() {
<Modal
title={editingPlugin ? '编辑插件' : '添加插件'}
open={modalVisible}
centered
onCancel={() => {
setModalVisible(false);
form.resetFields();
+45 -80
View File
@@ -73,23 +73,8 @@ export default function ProjectList() {
};
const handleEnterProject = (id: string) => {
const project = projects.find(p => p.id === id);
if (project) {
console.log('项目信息:', {
id: project.id,
title: project.title,
wizard_status: project.wizard_status,
wizard_step: project.wizard_step
});
if (project.wizard_status === 'incomplete' || !project.wizard_status) {
console.log('向导未完成,跳转到向导页面');
navigate(`/wizard?projectId=${id}&step=${project.wizard_step || 0}`);
} else {
console.log('向导已完成,进入项目管理界面');
navigate(`/project/${id}`);
}
}
// 简化后直接进入项目,不再检查向导状态
navigate(`/project/${id}`);
};
const getStatusTag = (status: string) => {
@@ -207,8 +192,8 @@ export default function ProjectList() {
setSelectedProjectIds([]);
};
// 获取可导出的项目(过滤掉向导未完成的项目)
const exportableProjects = projects.filter(p => p.wizard_status === 'completed');
// 获取所有可导出的项目
const exportableProjects = projects;
// 关闭导出对话框
const handleCloseExportModal = () => {
@@ -631,12 +616,11 @@ export default function ProjectList() {
<Row gutter={[16, 16]}>
{projects.map((project) => {
const progress = getProgress(project.current_words, project.target_words || 0);
const isWizardComplete = project.wizard_status === 'completed';
return (
<Col {...gridConfig} key={project.id}>
<Badge.Ribbon
text={isWizardComplete ? getStatusTag(project.status) : <Tag color="orange" icon={<RocketOutlined />}></Tag>}
text={getStatusTag(project.status)}
color="transparent"
style={{ top: 12, right: 12 }}
>
@@ -680,69 +664,50 @@ export default function ProjectList() {
{project.description || '暂无描述'}
</Paragraph>
{isWizardComplete ? (
<>
{project.target_words && project.target_words > 0 && (
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<Text strong style={{ fontSize: 12 }}>{progress}%</Text>
</div>
<Progress
percent={progress}
strokeColor={getProgressColor(progress)}
showInfo={false}
size={{ height: 8 }}
/>
</div>
)}
<Row gutter={12}>
<Col span={12}>
<div style={{
textAlign: 'center',
padding: '12px 0',
background: '#f5f5f5',
borderRadius: 8
}}>
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#1890ff' }}>
{(project.current_words / 1000).toFixed(1)}K
</div>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
</div>
</Col>
<Col span={12}>
<div style={{
textAlign: 'center',
padding: '12px 0',
background: '#f5f5f5',
borderRadius: 8
}}>
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#52c41a' }}>
{project.target_words ? (project.target_words / 1000).toFixed(0) + 'K' : '--'}
</div>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
</div>
</Col>
</Row>
</>
) : (
<div style={{
textAlign: 'center',
padding: '24px 0',
background: '#f5f5f5',
borderRadius: 8
}}>
<RocketOutlined style={{ fontSize: 32, color: '#faad14', marginBottom: 12 }} />
<div style={{ color: '#faad14', fontWeight: 'bold', marginBottom: 4 }}>
{project.target_words && project.target_words > 0 && (
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
<Text strong style={{ fontSize: 12 }}>{progress}%</Text>
</div>
<Text type="secondary" style={{ fontSize: 12 }}>
</Text>
<Progress
percent={progress}
strokeColor={getProgressColor(progress)}
showInfo={false}
size={{ height: 8 }}
/>
</div>
)}
<Row gutter={12}>
<Col span={12}>
<div style={{
textAlign: 'center',
padding: '12px 0',
background: '#f5f5f5',
borderRadius: 8
}}>
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#1890ff' }}>
{(project.current_words / 1000).toFixed(1)}K
</div>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
</div>
</Col>
<Col span={12}>
<div style={{
textAlign: 'center',
padding: '12px 0',
background: '#f5f5f5',
borderRadius: 8
}}>
<div style={{ fontSize: 20, fontWeight: 'bold', color: '#52c41a' }}>
{project.target_words ? (project.target_words / 1000).toFixed(0) + 'K' : '--'}
</div>
<Text type="secondary" style={{ fontSize: 12 }}></Text>
</div>
</Col>
</Row>
<div style={{
marginTop: 16,
paddingTop: 16,
File diff suppressed because it is too large Load Diff
+126 -6
View File
@@ -1,12 +1,18 @@
import { Card, Descriptions, Empty, Typography } from 'antd';
import { GlobalOutlined } from '@ant-design/icons';
import { Card, Descriptions, Empty, Typography, Button, Modal, Form, Input, message } from 'antd';
import { GlobalOutlined, EditOutlined } from '@ant-design/icons';
import { useState } from 'react';
import { useStore } from '../store';
import { cardStyles } from '../components/CardStyles';
import { projectApi } from '../services/api';
const { Title, Paragraph } = Typography;
const { TextArea } = Input;
export default function WorldSetting() {
const { currentProject } = useStore();
const { currentProject, setCurrentProject } = useStore();
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
const [editForm] = Form.useForm();
const [isSaving, setIsSaving] = useState(false);
if (!currentProject) return null;
@@ -62,10 +68,28 @@ export default function WorldSetting() {
marginBottom: 24,
borderBottom: '1px solid #f0f0f0',
display: 'flex',
alignItems: 'center'
alignItems: 'center',
justifyContent: 'space-between'
}}>
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: '#1890ff' }} />
<h2 style={{ margin: 0 }}></h2>
<div style={{ display: 'flex', alignItems: 'center' }}>
<GlobalOutlined style={{ fontSize: 24, marginRight: 12, color: '#1890ff' }} />
<h2 style={{ margin: 0 }}></h2>
</div>
<Button
type="primary"
icon={<EditOutlined />}
onClick={() => {
editForm.setFieldsValue({
world_time_period: currentProject.world_time_period || '',
world_location: currentProject.world_location || '',
world_atmosphere: currentProject.world_atmosphere || '',
world_rules: currentProject.world_rules || '',
});
setIsEditModalVisible(true);
}}
>
</Button>
</div>
{/* 可滚动内容区域 */}
@@ -182,6 +206,102 @@ export default function WorldSetting() {
</div>
</Card>
</div>
{/* 编辑世界观模态框 */}
<Modal
title="编辑世界观"
open={isEditModalVisible}
centered
onCancel={() => {
setIsEditModalVisible(false);
editForm.resetFields();
}}
onOk={async () => {
try {
const values = await editForm.validateFields();
setIsSaving(true);
const updatedProject = await projectApi.updateProject(currentProject.id, {
world_time_period: values.world_time_period,
world_location: values.world_location,
world_atmosphere: values.world_atmosphere,
world_rules: values.world_rules,
});
setCurrentProject(updatedProject);
message.success('世界观更新成功');
setIsEditModalVisible(false);
editForm.resetFields();
} catch (error) {
console.error('更新世界观失败:', error);
message.error('更新失败,请重试');
} finally {
setIsSaving(false);
}
}}
confirmLoading={isSaving}
width={800}
okText="保存"
cancelText="取消"
>
<Form
form={editForm}
layout="vertical"
style={{ marginTop: 16 }}
>
<Form.Item
label="时间设定"
name="world_time_period"
rules={[{ required: true, message: '请输入时间设定' }]}
>
<TextArea
rows={4}
placeholder="描述故事发生的时代背景..."
showCount
maxLength={1000}
/>
</Form.Item>
<Form.Item
label="地点设定"
name="world_location"
rules={[{ required: true, message: '请输入地点设定' }]}
>
<TextArea
rows={4}
placeholder="描述故事发生的地理位置和环境..."
showCount
maxLength={1000}
/>
</Form.Item>
<Form.Item
label="氛围设定"
name="world_atmosphere"
rules={[{ required: true, message: '请输入氛围设定' }]}
>
<TextArea
rows={4}
placeholder="描述故事的整体氛围和基调..."
showCount
maxLength={1000}
/>
</Form.Item>
<Form.Item
label="规则设定"
name="world_rules"
rules={[{ required: true, message: '请输入规则设定' }]}
>
<TextArea
rows={4}
placeholder="描述这个世界的特殊规则和设定..."
showCount
maxLength={1000}
/>
</Form.Item>
</Form>
</Modal>
</div>
);
}
+30
View File
@@ -123,10 +123,23 @@ export const authApi = {
localLogin: (username: string, password: string) =>
api.post<unknown, { success: boolean; message: string; user: User }>('/auth/local/login', { username, password }),
bindAccountLogin: (username: string, password: string) =>
api.post<unknown, { success: boolean; message: string; user: User }>('/auth/bind/login', { username, password }),
getLinuxDOAuthUrl: () => api.get<unknown, AuthUrlResponse>('/auth/linuxdo/url'),
getCurrentUser: () => api.get<unknown, User>('/auth/user'),
getPasswordStatus: () => api.get<unknown, {
has_password: boolean;
has_custom_password: boolean;
username: string | null;
default_password: string | null;
}>('/auth/password/status'),
setPassword: (password: string) =>
api.post<unknown, { success: boolean; message: string }>('/auth/password/set', { password }),
refreshSession: () => api.post<unknown, { message: string; expire_at: number; remaining_minutes: number }>('/auth/refresh'),
logout: () => api.post('/auth/logout'),
@@ -306,6 +319,23 @@ export const chapterApi = {
checkCanGenerate: (chapterId: string) =>
api.get<unknown, import('../types').ChapterCanGenerateResponse>(`/chapters/${chapterId}/can-generate`),
// 章节重新生成相关
getRegenerationTasks: (chapterId: string, limit?: number) =>
api.get<unknown, {
chapter_id: string;
total: number;
tasks: Array<{
task_id: string;
status: string;
version_number: number | null;
version_note: string | null;
original_word_count: number | null;
regenerated_word_count: number | null;
created_at: string | null;
completed_at: string | null;
}>;
}>(`/chapters/${chapterId}/regeneration/tasks`, { params: { limit } }),
};
export const writingStyleApi = {
+16 -5
View File
@@ -2,6 +2,7 @@ export interface SSEMessage {
type: 'progress' | 'chunk' | 'result' | 'error' | 'done';
message?: string;
progress?: number;
word_count?: number;
status?: 'processing' | 'success' | 'error' | 'warning';
content?: string;
data?: any;
@@ -10,7 +11,7 @@ export interface SSEMessage {
}
export interface SSEClientOptions {
onProgress?: (message: string, progress: number, status: string) => void;
onProgress?: (message: string, progress: number, status: string, wordCount?: number) => void;
onChunk?: (content: string) => void;
onResult?: (data: any) => void;
onError?: (error: string, code?: number) => void;
@@ -61,8 +62,13 @@ export class SSEClient {
private handleMessage(message: SSEMessage, resolve: Function, reject: Function) {
switch (message.type) {
case 'progress':
if (this.options.onProgress && message.message && message.progress !== undefined) {
this.options.onProgress(message.message, message.progress, message.status || 'processing');
if (this.options.onProgress && message.progress !== undefined) {
this.options.onProgress(
message.message || '',
message.progress,
message.status || 'processing',
message.word_count
);
}
break;
@@ -201,8 +207,13 @@ export class SSEPostClient {
private async handleMessage(message: SSEMessage, resolve: Function, reject: Function) {
switch (message.type) {
case 'progress':
if (this.options.onProgress && message.message && message.progress !== undefined) {
this.options.onProgress(message.message, message.progress, message.status || 'processing');
if (this.options.onProgress && message.progress !== undefined) {
this.options.onProgress(
message.message || '',
message.progress,
message.status || 'processing',
message.word_count
);
}
break;