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