# AI 建议回复功能重构说明 ## 重构日期 2025-11-17 ## 最新更新 2025-11-17 - 优化请求逻辑,确保每次只发送 3 次 API 请求 ## 问题描述 重构前的 AI 建议功能存在以下问题: 1. **状态管理复杂**:维护了大量状态(`allSuggestions`、`loadedPages`、`batchNo`、`excContentList`、`isPageLoading`、`isRequesting` 等) 2. **页面切换逻辑复杂**:需要跟踪哪些页面已加载,切换时判断是否显示骨架屏 3. **显示逻辑不清晰**:`isLoading` 只在首次加载时为 true,分页切换时骨架屏显示不正确 4. **用户体验问题**:会出现 AI 辅助提示为空的情况(如图所示) ## 重构目标 1. **简化状态管理**:只保留必要的状态 2. **统一交互逻辑**:每次打开建议面板时,重置到第1页并重新获取数据 3. **清晰的骨架屏显示**:数据未加载完成时始终显示骨架屏 ## 重构内容 ### 1. `useAiReplySuggestions` Hook 重构 #### 状态简化 **重构前:** ```typescript const [allSuggestions, setAllSuggestions] = useState([]); const [loadedPages, setLoadedPages] = useState>(new Set([1])); const [isPageLoading, setIsPageLoading] = useState(false); const [isRequesting, setIsRequesting] = useState(false); // ... 其他状态 ``` **重构后:** ```typescript const [pageData, setPageData] = useState>(new Map()); const [loadingPages, setLoadingPages] = useState>(new Set()); // ... 其他必要状态 ``` #### 核心逻辑变化 **重构前:** - `showSuggestions` 函数会根据多种条件判断是否需要获取数据 - 首次获取第1页数据,然后静默获取第2、3页 - 页面切换时复杂的加载状态判断 **重构后:** - **每次调用 `showSuggestions` 都重置到第1页并重新获取所有数据** - 按页存储数据到 `Map` 中,逻辑更清晰 - 页面切换时自动检查并加载缺失的页面数据 ### 2. 数据获取流程优化 #### 重构前流程 1. 获取第1页 → 立即展示 2. 静默获取第2页 → 更新 `loadedPages` 3. 静默获取第3页 → 更新 `loadedPages` 4. 用户切换页面时检查 `loadedPages` #### 重构后流程 1. 清空旧数据 2. 获取第1页 → 存入 `pageData.set(1, ...)` 3. 获取第2页 → 存入 `pageData.set(2, ...)` 4. 获取第3页 → 存入 `pageData.set(3, ...)` 5. 用户切换页面时从 `pageData` 读取,无数据则显示骨架屏 ### 3. 骨架屏显示逻辑 **重构前:** ```typescript const displaySuggestions = isCurrentPageLoaded ? suggestions : Array.from({ length: 3 }, ...); ``` **重构后:** ```typescript const suggestions = isCurrentPageLoading || !currentPageSuggestions ? Array.from({ length: 3 }, (_, index) => ({ id: `skeleton-${currentPage}-${index}`, text: '', isSkeleton: true })) : currentPageSuggestions; ``` UI 组件根据 `isSkeleton` 标志渲染骨架屏或真实内容。 ### 4. UI 组件更新 `AiReplySuggestions.tsx` 更新: - 添加 `isSkeleton` 字段到 `ReplySuggestion` 接口 - 检查 `suggestions.some(s => s.isSkeleton)` 决定是否显示骨架屏 - 骨架屏添加 `animate-pulse` 动画效果 ## 重构优势 ### 1. **简化的状态管理** - 使用 `Map` 按页存储数据,结构清晰 - 移除了 `isRequesting`、`isPageLoading`、`isCurrentPageLoaded` 等冗余状态 - 只需维护 `loadingPages: Set` 来跟踪正在加载的页面 ### 2. **智能的数据管理** - **有新 AI 消息时**:重置到第1页并重新获取数据 - **没有新消息时**:保留之前的建议内容和当前页面位置 - 避免了"空白建议"的问题 - 数据加载状态清晰可见(骨架屏动画) ### 3. **更可靠的数据加载** - 智能判断是否需要重新获取数据(基于最后一条 AI 消息 ID) - 首次打开或数据为空时自动获取 - 减少了因状态不同步导致的 bug ### 4. **更好的代码可维护性** - 代码从 312 行减少到 200 行 - 逻辑流程更清晰直观 - 减少了状态依赖和副作用 ## 使用示例 ### 场景1:首次打开建议面板(渐进式加载) 1. **点击建议按钮** → 面板打开,重置到第1页,显示骨架屏 2. **~500ms 后第1页数据返回** → ✨ **立即展示第1页的3条建议**(用户可以开始浏览) 3. **后台继续获取第2、3页** → 不阻塞用户交互 4. **点击下一页** → 切换到第2页 - 如果第2页数据已加载 → 立即显示 - 如果第2页数据未加载 → 显示骨架屏(通常第2页已在后台加载完成) ### 场景2:关闭后再次打开(无新消息) 1. **关闭建议面板** 2. **再次点击建议按钮** → 面板打开 3. **保留上次的内容和页面位置**(比如之前在第2页,现在仍在第2页) 4. **无需重新加载数据**,立即显示缓存的建议 ### 场景3:收到新 AI 消息后打开 1. **用户发送消息,AI 回复** 2. **点击建议按钮** → 面板打开 3. **检测到新的 AI 消息** → 重置到第1页,显示骨架屏 4. **重新获取所有建议数据**(基于最新的对话内容) ### 场景4:面板已打开时收到新 AI 消息 1. **建议面板已打开**(比如用户正在浏览第2页) 2. **用户发送消息,AI 回复** 3. **自动刷新建议数据** → **保持在当前页面**(第2页),不强制切回第1页 4. **渐进式加载新数据**,用户可以继续浏览当前页 ## API 返回值变化 Hook 返回的接口保持不变,确保向后兼容: ```typescript return { suggestions, // ReplySuggestion[] (包含 isSkeleton 标志) currentPage, // number totalPages, // number isLoading, // boolean (只要有页面在加载就为 true) isVisible, // boolean showSuggestions, // () => void hideSuggestions, // () => void handlePageChange, // (page: number) => void }; ``` 移除的返回值: - `isPageLoading` - `isCurrentPageLoaded` - `refreshSuggestions` ## 关键优化点 ### 避免多次请求的设计 **问题**:原始实现会触发多次重复请求 **解决方案**: 1. **统一的 `fetchAllData` 函数**:一次性顺序获取3页数据,使用局部变量传递 `batchNo` 和 `excContentList` 2. **防重复调用保护**:在 `fetchAllData` 开始时检查 `loadingPages.size > 0`,如果已有加载则跳过 3. **移除分页独立加载**:删除了 `fetchPageData` 函数和 `handlePageChange` 中的数据获取逻辑 4. **简化页面切换**:`handlePageChange` 只负责切换 `currentPage`,不触发数据加载 ### 渐进式加载流程 采用**渐进式加载**策略,让用户尽早看到数据,提升体验: ```typescript fetchAllData() { // 1. 检查防重复 if (loadingPages.size > 0) return; // 2. 标记所有页面为加载中 setLoadingPages(new Set([1, 2, 3])); // 3. 获取第1页 → 立即展示 const response1 = await genSupContentV2({ aiId, excContentList: [] }); setPageData(new Map([[1, response1]])); // ✨ 立即展示第1页 setLoadingPages(new Set([2, 3])); // 标记第1页已完成 // 4. 获取第2页 → 追加展示 const response2 = await genSupContentV2({ aiId, batchNo, excContentList: [response1] }); setPageData(prev => prev.set(2, response2)); // ✨ 追加第2页 setLoadingPages(new Set([3])); // 标记第2页已完成 // 5. 获取第3页 → 追加展示 const response3 = await genSupContentV2({ aiId, batchNo, excContentList: [response1, response2] }); setPageData(prev => prev.set(3, response3)); // ✨ 追加第3页 setLoadingPages(new Set()); // 全部完成 } ``` **关键优势**: - 🚀 **第1页数据到达后立即展示**,用户无需等待所有数据 - 📊 **后续页面数据追加展示**,不影响用户浏览第1页 - ⏱️ **感知加载时间更短**,提升用户体验 - 🔄 **页面2、3可以并行渲染**,用户切换时自动显示骨架屏 ## 注意事项 1. **精确的请求次数**:每次调用 `fetchAllData` 只会发送 **3 次** API 请求(第1、2、3页) 2. **智能缓存策略**:没有新 AI 消息时,复用已有数据,不发送请求 3. **网络失败处理**:任一页面加载失败会中断整个流程并清空数据 4. **Coin 不足处理**:任何页面触发 Coin 不足错误都会关闭整个建议面板 5. **防重复保护**:通过 `loadingPages.size` 检查防止并发调用 ## 测试建议 1. 测试正常流程:打开建议 → 浏览3页 → 关闭 → 再次打开 2. 测试网络慢场景:确认骨架屏正确显示 3. 测试 Coin 不足场景:确认面板正确关闭 4. 测试新消息场景:发送消息后面板已打开时自动刷新