update:1.优化世界观生成提示词 2.修复章节分析页面内容重复问题 3.限制mcp调用最大并发数
This commit is contained in:
@@ -95,15 +95,15 @@ async def world_building_generator(
|
||||
3. 相关领域的专业知识
|
||||
4. 类似作品的设定参考
|
||||
|
||||
请根据题材特点,有针对性地查询2-3个关键问题。"""
|
||||
请查询最关键的1个问题(不要超过1个)。"""
|
||||
|
||||
# 调用MCP增强的AI(非流式,最多2轮工具调用)
|
||||
# 调用MCP增强的AI(非流式,最多1轮工具调用,避免超时)
|
||||
planning_result = await user_ai_service.generate_text_with_mcp(
|
||||
prompt=planning_prompt,
|
||||
user_id=user_id,
|
||||
db_session=db,
|
||||
enable_mcp=True,
|
||||
max_tool_rounds=2,
|
||||
max_tool_rounds=1,
|
||||
tool_choice="auto",
|
||||
provider=None,
|
||||
model=None
|
||||
@@ -365,15 +365,15 @@ async def characters_generator(
|
||||
3. 职业特点和生活方式
|
||||
4. 相关领域的人物原型
|
||||
|
||||
请根据题材特点,有针对性地查询1-2个关键问题。"""
|
||||
请查询最关键的1个问题(不要超过1个)。"""
|
||||
|
||||
# 调用MCP增强的AI(非流式,最多2轮工具调用)
|
||||
# 调用MCP增强的AI(非流式,最多1轮工具调用,避免超时)
|
||||
planning_result = await user_ai_service.generate_text_with_mcp(
|
||||
prompt=planning_prompt,
|
||||
user_id=user_id,
|
||||
db_session=db,
|
||||
enable_mcp=True,
|
||||
max_tool_rounds=2,
|
||||
max_tool_rounds=1, # ✅ 优化: 从2轮减少到1轮
|
||||
tool_choice="auto",
|
||||
provider=None,
|
||||
model=None
|
||||
|
||||
@@ -73,10 +73,10 @@ def _get_or_create_http_client(
|
||||
|
||||
client = httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(
|
||||
connect=60.0, # 连接超时
|
||||
read=180.0, # 读取超时
|
||||
write=60.0, # 写入超时
|
||||
pool=60.0 # 连接池超时
|
||||
connect=90.0, # 连接超时
|
||||
read=300.0, # 读取超时
|
||||
write=90.0, # 写入超时
|
||||
pool=90.0 # 连接池超时
|
||||
),
|
||||
limits=limits,
|
||||
headers={
|
||||
@@ -959,7 +959,7 @@ class AIService:
|
||||
|
||||
else:
|
||||
# 达到最大轮次
|
||||
logger.warning(f"达到MCP最大调用轮次 {max_tool_rounds}")
|
||||
logger.info(f"达到MCP最大调用轮次 {max_tool_rounds}")
|
||||
result["content"] = conversation_history[-1].get("content", "")
|
||||
result["finish_reason"] = "max_rounds"
|
||||
|
||||
|
||||
@@ -291,16 +291,18 @@ class MCPToolService:
|
||||
user_id: str,
|
||||
tool_calls: List[Dict[str, Any]],
|
||||
db_session: AsyncSession,
|
||||
timeout: Optional[float] = None
|
||||
timeout: Optional[float] = None,
|
||||
max_concurrent: int = 2
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
批量执行AI请求的工具调用(并行执行)
|
||||
批量执行AI请求的工具调用(限制并发数,避免超时)
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
tool_calls: AI返回的工具调用列表
|
||||
db_session: 数据库会话
|
||||
timeout: 单个工具调用的超时时间(秒,默认使用配置)
|
||||
max_concurrent: 最大并发工具调用数(默认2)
|
||||
|
||||
Returns:
|
||||
工具调用结果列表
|
||||
@@ -311,41 +313,54 @@ class MCPToolService:
|
||||
# 使用配置的默认超时
|
||||
actual_timeout = timeout or mcp_config.TOOL_CALL_TIMEOUT_SECONDS
|
||||
|
||||
logger.info(f"开始执行 {len(tool_calls)} 个工具调用 (超时={actual_timeout}s)")
|
||||
logger.info(f"开始执行 {len(tool_calls)} 个工具调用 (超时={actual_timeout}s, 最大并发={max_concurrent})")
|
||||
|
||||
# 创建异步任务列表
|
||||
tasks = [
|
||||
self._execute_single_tool(
|
||||
user_id=user_id,
|
||||
tool_call=tool_call,
|
||||
db_session=db_session,
|
||||
timeout=actual_timeout
|
||||
)
|
||||
for tool_call in tool_calls
|
||||
]
|
||||
# ✅ 分批执行,每批最多max_concurrent个
|
||||
all_results = []
|
||||
for i in range(0, len(tool_calls), max_concurrent):
|
||||
batch = tool_calls[i:i+max_concurrent]
|
||||
batch_num = i // max_concurrent + 1
|
||||
total_batches = (len(tool_calls) + max_concurrent - 1) // max_concurrent
|
||||
|
||||
# 并行执行所有工具调用
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
logger.info(f"执行工具批次 {batch_num}/{total_batches}, 数量: {len(batch)}")
|
||||
|
||||
# 处理结果
|
||||
formatted_results = []
|
||||
for i, result in enumerate(results):
|
||||
tool_call = tool_calls[i]
|
||||
# 创建当前批次的异步任务
|
||||
tasks = [
|
||||
self._execute_single_tool(
|
||||
user_id=user_id,
|
||||
tool_call=tool_call,
|
||||
db_session=db_session,
|
||||
timeout=actual_timeout
|
||||
)
|
||||
for tool_call in batch
|
||||
]
|
||||
|
||||
if isinstance(result, Exception):
|
||||
# 工具调用异常
|
||||
formatted_results.append({
|
||||
"tool_call_id": tool_call.get("id", f"call_{i}"),
|
||||
"role": "tool",
|
||||
"name": tool_call["function"]["name"],
|
||||
"content": f"工具调用失败: {str(result)}",
|
||||
"success": False,
|
||||
"error": str(result)
|
||||
})
|
||||
else:
|
||||
formatted_results.append(result)
|
||||
# 并行执行当前批次
|
||||
batch_results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
return formatted_results
|
||||
# 处理批次结果
|
||||
for j, result in enumerate(batch_results):
|
||||
tool_call = batch[j]
|
||||
|
||||
if isinstance(result, Exception):
|
||||
# 工具调用异常
|
||||
all_results.append({
|
||||
"tool_call_id": tool_call.get("id", f"call_{i+j}"),
|
||||
"role": "tool",
|
||||
"name": tool_call["function"]["name"],
|
||||
"content": f"工具调用失败: {str(result)}",
|
||||
"success": False,
|
||||
"error": str(result)
|
||||
})
|
||||
else:
|
||||
all_results.append(result)
|
||||
|
||||
# 批次间增加短暂延迟,避免API限流
|
||||
if i + max_concurrent < len(tool_calls):
|
||||
await asyncio.sleep(0.5)
|
||||
logger.debug(f"批次间延迟 0.5 秒...")
|
||||
|
||||
return all_results
|
||||
|
||||
async def _execute_single_tool(
|
||||
self,
|
||||
|
||||
@@ -119,27 +119,38 @@ class PromptService:
|
||||
主题:{theme}
|
||||
类型:{genre}
|
||||
|
||||
# 2. 世界构建框架
|
||||
请生成包含以下四个核心板块的世界构建框架。请确保所有板块都围绕【核心概念】展开,并且板块之间【互为因果】。
|
||||
# 2. 核心指令(CRITICAL)
|
||||
* **去标签化**:严禁使用通用的“XX纪元”、“XX时代”、“XX年”作为时间背景的开头或核心描述。请直接描述世界所处的状态、技术水平或生存现状。
|
||||
* **动态演绎**:所有设定必须直接由输入的主题衍生而来。例如,如果是赛博朋克,不要只写“高科技”,要写“义体技术如何导致了贫民窟的特定生活方式”。
|
||||
* **拒绝陈词滥调**:避免使用宏大的空洞词汇,专注于具体的、可感知的细节。
|
||||
|
||||
1. **时间背景 (time_period)**:
|
||||
* 具体的时代设定(例如:星际航行晚期、黑铁时代)。
|
||||
* 重要的【历史转折事件】(是什么导致了当前的世界面貌?)。
|
||||
* 当前的主要【社会矛盾】或【时代议题】。
|
||||
2. **地理/空间 (location)**:
|
||||
* 主要舞台(如城市、星球、位面)的【地理环境特征】。
|
||||
* 这些特征如何影响了【文明】的形态和【资源】分布?
|
||||
* 独特的【空间布局】或【奇观】。
|
||||
3. **氛围基调 (atmosphere)**:
|
||||
* 整体的【情感色彩】(例如:压抑、荒诞、史诗、诡异)。
|
||||
* 【视觉风格】(例如:赛博霓虹、蒸汽朋克、哥特式)。
|
||||
* 普通居民在日常生活中最常【感受】到什么?
|
||||
4. **世界规则 (rules)**:
|
||||
* 【物理法则】或【超自然力量】(如魔法、科技)的【具体运作方式】和【代价】。
|
||||
* 【社会规则】和【权力结构】(谁在统治?基于什么?)。
|
||||
* 最严重的【社会禁忌】是什么?违反了会怎样?
|
||||
# 3. 世界构建框架
|
||||
请生成包含以下四个核心板块的JSON。请确保所有板块互为因果,逻辑严密。
|
||||
|
||||
# 3. 严格格式要求
|
||||
**重要说明:每个字段的value必须是一个完整的文本字符串,将以下所有要点整合成连贯的段落描述,不要使用嵌套的JSON对象或数组。**
|
||||
|
||||
1. **time_period (时间线与文明阶段)**:
|
||||
请将以下内容整合为一段完整的文字描述(300-500字):
|
||||
* 描述当前世界处于什么**发展阶段**(是毁灭边缘、新生萌芽、还是停滞不前?),**不要给这个阶段起名字**,而是描述其**特征**。
|
||||
* **历史转折点**:具体的事件(战争、发明、灾难),它如何直接导致了现在的局面?
|
||||
* **当下的核心矛盾**:时间流逝带来的具体焦虑是什么?(例如:资源枯竭的倒计时、某种信仰的崩塌)。
|
||||
2. **location (空间与生态环境)**:
|
||||
请将以下内容整合为一段完整的文字描述(300-500字):
|
||||
* **舞台特征**:描述主要故事发生的地理或空间环境(如:悬浮的破碎岛屿、被真菌覆盖的地铁网络)。
|
||||
* **环境与生存**:地理环境如何强迫居民改变了生活方式?(例如:因为引力失衡,建筑都是倒挂的)。
|
||||
* **标志性奇观**:一个能代表这个世界独特性的具体场景或建筑。
|
||||
3. **atmosphere (感官体验与基调)**:
|
||||
请将以下内容整合为一段完整的文字描述(300-500字):
|
||||
* **感官细节**:如果站在这个世界的街头,会**闻**到什么?**听**到什么?(不要只写"压抑",要写"空气中弥漫着铁锈和合成营养膏的酸味")。
|
||||
* **视觉美学**:描述具体的色彩倾向和光影质感。
|
||||
* **居民心态**:普通人普遍的心理状态(是麻木、狂热、还是某种特定的恐惧)。
|
||||
4. **rules (运作逻辑与禁忌)**:
|
||||
请将以下内容整合为一段完整的文字描述(300-500字):
|
||||
* **核心法则**:这个世界运行的底层逻辑(物理、魔法或科技)。**重点描述代价**(使用力量需要支付什么?)。
|
||||
* **权力架构**:谁掌握资源?他们通过什么手段维持控制(暴力、技术垄断、宗教洗脑)?
|
||||
* **红线禁忌**:这个社会绝对不能触碰的具体底线,以及违反后的直接后果。
|
||||
|
||||
# 4. 严格格式要求
|
||||
1. **绝对纯净JSON**:你的[唯一]输出必须是一个完整的JSON对象。输出必须以左花括号开始,并以右花括号结束。
|
||||
2. **禁止额外字符**:不要在JSON对象之前或之后包含任何说明文字、Markdown标记(如三个反引号加json)、注释或任何其他非JSON字符。
|
||||
3. **JSON内部文本规则**:在JSON的value字符串内部:
|
||||
@@ -147,13 +158,14 @@ class PromptService:
|
||||
* 所有【专有名词】(如地点、人物、组织)应使用【】包裹。
|
||||
* 所有《作品》或《特殊概念》的标题应使用《》包裹。
|
||||
4. **JSON结构**:严格遵守`"key": "value"`的英文双引号结构,并使用下面指定的key。
|
||||
5. **内容密度**:每个字段的描述都必须【深入且详实】,提供至少5-7个具体的设定点或细节。
|
||||
5. **内容密度**:每个字段的描述都必须【深入且详实】,提供至少5-7个具体的设定点或细节,整合为连贯的段落文本。
|
||||
6. **禁止嵌套结构**:value必须是纯文本字符串,绝对不能是JSON对象或数组,所有信息都要整合在一个字符串中。
|
||||
|
||||
{{
|
||||
"time_period": "(此处填写时间背景的详细描述)",
|
||||
"location": "(此处填写地理/空间的详细描述)",
|
||||
"atmosphere": "(此处填写氛围基调的详细描述)",
|
||||
"rules": "(此处填写世界规则的详细描述)"
|
||||
"time_period": "(此处填写一段完整的文字描述,包含发展阶段特征、历史转折点、核心矛盾等内容,300-500字)",
|
||||
"location": "(此处填写一段完整的文字描述,包含舞台特征、环境与生存、标志性奇观等内容,300-500字)",
|
||||
"atmosphere": "(此处填写一段完整的文字描述,包含感官细节、视觉美学、居民心态等内容,300-500字)",
|
||||
"rules": "(此处填写一段完整的文字描述,包含核心法则、权力架构、红线禁忌等内容,300-500字)"
|
||||
}}"""
|
||||
|
||||
# 批量角色生成提示词
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
# 安装和测试指南
|
||||
|
||||
## 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. **智能推荐**
|
||||
- 基于历史生成结果推荐最佳配置
|
||||
- 学习用户偏好自动调整参数
|
||||
@@ -24,6 +24,7 @@ interface TextSegment {
|
||||
type: 'text' | 'annotated';
|
||||
content: string;
|
||||
annotation?: MemoryAnnotation;
|
||||
annotations?: MemoryAnnotation[]; // 🔧 支持多个标注
|
||||
}
|
||||
|
||||
interface AnnotatedTextProps {
|
||||
@@ -108,30 +109,89 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
const result: TextSegment[] = [];
|
||||
let lastPos = 0;
|
||||
|
||||
// 🔧 智能分组:检测重叠和相邻的标注
|
||||
const annotationRanges: Array<{
|
||||
start: number;
|
||||
end: number;
|
||||
annotations: MemoryAnnotation[];
|
||||
}> = [];
|
||||
|
||||
for (const annotation of processedAnnotations) {
|
||||
const { position, length } = annotation;
|
||||
const actualLength = length > 0 ? length : 30;
|
||||
const start = position;
|
||||
const end = position + actualLength;
|
||||
|
||||
// 添加普通文本片段
|
||||
if (position > lastPos) {
|
||||
// 查找是否有重叠或紧邻的范围
|
||||
const overlappingRange = annotationRanges.find(
|
||||
(range) =>
|
||||
(start >= range.start && start <= range.end) || // 起始点在范围内
|
||||
(end >= range.start && end <= range.end) || // 结束点在范围内
|
||||
(start <= range.start && end >= range.end) || // 完全包含
|
||||
Math.abs(start - range.end) <= 5 || // 紧邻(容差5字符)
|
||||
Math.abs(end - range.start) <= 5
|
||||
);
|
||||
|
||||
if (overlappingRange) {
|
||||
// 合并到现有范围
|
||||
overlappingRange.start = Math.min(overlappingRange.start, start);
|
||||
overlappingRange.end = Math.max(overlappingRange.end, end);
|
||||
overlappingRange.annotations.push(annotation);
|
||||
} else {
|
||||
// 创建新范围
|
||||
annotationRanges.push({
|
||||
start,
|
||||
end,
|
||||
annotations: [annotation],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按起始位置排序
|
||||
annotationRanges.sort((a, b) => a.start - b.start);
|
||||
|
||||
// 🔧 智能分片:将重叠区域分成多个小片段
|
||||
for (const range of annotationRanges) {
|
||||
// 添加前面的普通文本
|
||||
if (range.start > lastPos) {
|
||||
result.push({
|
||||
type: 'text',
|
||||
content: content.slice(lastPos, position),
|
||||
content: content.slice(lastPos, range.start),
|
||||
});
|
||||
}
|
||||
|
||||
// 添加标注片段
|
||||
const annotatedContent = content.slice(
|
||||
position,
|
||||
position + (length > 0 ? length : 30) // 如果没有长度,默认30字符
|
||||
);
|
||||
if (range.annotations.length === 1) {
|
||||
// 单个标注,直接添加
|
||||
result.push({
|
||||
type: 'annotated',
|
||||
content: content.slice(range.start, range.end),
|
||||
annotation: range.annotations[0],
|
||||
annotations: range.annotations,
|
||||
});
|
||||
} else {
|
||||
// 🔧 多个标注:将文本分成多个小片段
|
||||
const totalLength = range.end - range.start;
|
||||
const segmentLength = Math.max(1, Math.floor(totalLength / range.annotations.length));
|
||||
|
||||
result.push({
|
||||
type: 'annotated',
|
||||
content: annotatedContent,
|
||||
annotation,
|
||||
});
|
||||
// 按重要性排序标注
|
||||
const sortedAnnotations = [...range.annotations].sort((a, b) => b.importance - a.importance);
|
||||
|
||||
lastPos = position + (length > 0 ? length : 30);
|
||||
for (let i = 0; i < sortedAnnotations.length; i++) {
|
||||
const segmentStart = range.start + i * segmentLength;
|
||||
const segmentEnd = i === sortedAnnotations.length - 1
|
||||
? range.end
|
||||
: range.start + (i + 1) * segmentLength;
|
||||
|
||||
result.push({
|
||||
type: 'annotated',
|
||||
content: content.slice(segmentStart, segmentEnd),
|
||||
annotation: sortedAnnotations[i],
|
||||
annotations: sortedAnnotations, // 保留所有标注信息
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
lastPos = range.end;
|
||||
}
|
||||
|
||||
// 添加剩余文本
|
||||
@@ -142,6 +202,7 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`AnnotatedText: 处理${processedAnnotations.length}个标注,生成${result.length}个片段`);
|
||||
return result;
|
||||
}, [content, processedAnnotations]);
|
||||
|
||||
@@ -151,43 +212,73 @@ const AnnotatedText: React.FC<AnnotatedTextProps> = ({
|
||||
return <span key={index}>{segment.content}</span>;
|
||||
}
|
||||
|
||||
const { annotation } = segment;
|
||||
const { annotation, annotations } = segment;
|
||||
if (!annotation) return null;
|
||||
|
||||
const color = TYPE_COLORS[annotation.type];
|
||||
const icon = TYPE_ICONS[annotation.type];
|
||||
const isActive = activeAnnotationId === annotation.id;
|
||||
|
||||
// 工具提示内容
|
||||
// 🔧 工具提示内容:如果有多个标注,显示所有标注信息
|
||||
const tooltipContent = (
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
|
||||
{icon} {annotation.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.9 }}>
|
||||
{annotation.content.slice(0, 100)}
|
||||
{annotation.content.length > 100 ? '...' : ''}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 11, opacity: 0.7 }}>
|
||||
重要性: {(annotation.importance * 10).toFixed(1)}/10
|
||||
</div>
|
||||
{annotation.tags && annotation.tags.length > 0 && (
|
||||
<div style={{ marginTop: 4, fontSize: 11 }}>
|
||||
{annotation.tags.map((tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 3,
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
<div style={{ maxWidth: 350 }}>
|
||||
{annotations && annotations.length > 1 ? (
|
||||
// 多个标注
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 8, borderBottom: '1px solid rgba(255,255,255,0.3)', paddingBottom: 4 }}>
|
||||
📍 此处有 {annotations.length} 个标注
|
||||
</div>
|
||||
{annotations.map((ann, idx) => (
|
||||
<div key={ann.id} style={{
|
||||
marginBottom: idx < annotations.length - 1 ? 8 : 0,
|
||||
paddingBottom: idx < annotations.length - 1 ? 8 : 0,
|
||||
borderBottom: idx < annotations.length - 1 ? '1px solid rgba(255,255,255,0.1)' : 'none'
|
||||
}}>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 4, fontSize: 13 }}>
|
||||
{TYPE_ICONS[ann.type]} {ann.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 11, opacity: 0.9 }}>
|
||||
{ann.content.slice(0, 80)}
|
||||
{ann.content.length > 80 ? '...' : ''}
|
||||
</div>
|
||||
<div style={{ marginTop: 4, fontSize: 10, opacity: 0.7 }}>
|
||||
重要性: {(ann.importance * 10).toFixed(1)}/10
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// 单个标注
|
||||
<div>
|
||||
<div style={{ fontWeight: 'bold', marginBottom: 4 }}>
|
||||
{icon} {annotation.title}
|
||||
</div>
|
||||
<div style={{ fontSize: 12, opacity: 0.9 }}>
|
||||
{annotation.content.slice(0, 100)}
|
||||
{annotation.content.length > 100 ? '...' : ''}
|
||||
</div>
|
||||
<div style={{ marginTop: 8, fontSize: 11, opacity: 0.7 }}>
|
||||
重要性: {(annotation.importance * 10).toFixed(1)}/10
|
||||
</div>
|
||||
{annotation.tags && annotation.tags.length > 0 && (
|
||||
<div style={{ marginTop: 4, fontSize: 11 }}>
|
||||
{annotation.tags.map((tag, i) => (
|
||||
<span
|
||||
key={i}
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background: 'rgba(255,255,255,0.2)',
|
||||
padding: '2px 6px',
|
||||
borderRadius: 3,
|
||||
marginRight: 4,
|
||||
}}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -57,12 +57,9 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
};
|
||||
}, [visible, chapterId]);
|
||||
|
||||
const fetchAnalysisStatus = async () => {
|
||||
// 🔧 新增:独立的章节信息加载函数
|
||||
const loadChapterInfo = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 同时获取章节信息
|
||||
const chapterResponse = await fetch(`/api/chapters/${chapterId}`);
|
||||
if (chapterResponse.ok) {
|
||||
const chapterData = await chapterResponse.json();
|
||||
@@ -71,7 +68,20 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
chapter_number: chapterData.chapter_number,
|
||||
content: chapterData.content || ''
|
||||
});
|
||||
console.log('✅ 已刷新章节内容,字数:', chapterData.content?.length || 0);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 加载章节信息失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchAnalysisStatus = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 🔧 使用独立的章节加载函数
|
||||
await loadChapterInfo();
|
||||
|
||||
const response = await fetch(`/api/chapters/${chapterId}/analysis/status`);
|
||||
|
||||
@@ -134,6 +144,8 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
if (taskData.status === 'completed') {
|
||||
clearInterval(pollInterval);
|
||||
await fetchAnalysisResult();
|
||||
// 🔧 分析完成后刷新章节内容,确保显示最新内容
|
||||
await loadChapterInfo();
|
||||
} else if (taskData.status === 'failed') {
|
||||
clearInterval(pollInterval);
|
||||
setError(taskData.error_message || '分析失败');
|
||||
@@ -152,6 +164,9 @@ export default function ChapterAnalysis({ chapterId, visible, onClose }: Chapter
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
// 🔧 触发分析前先刷新章节内容,确保分析的是最新内容
|
||||
await loadChapterInfo();
|
||||
|
||||
const response = await fetch(`/api/chapters/${chapterId}/analyze`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user